Use read_table() to import tab-separated or other delimited files. Define the separator, heading and field types, then inspect records before analysis.
read_table() defaults to the tab delimiter. Tabs separate fields; line endings separate records. The method is supported in current Pandas. read_csv() with sep="\t" is an equivalent way to read TSV data. The extension alone does not determine the separator.
import pandas as pd
import tempfile
from pathlib import Path
from io import StringIO, BytesIO
student_csv = 'id,name,class,mark,gender\n1,John Deo,Four,75,female\n2,Max Ruin,Three,85,male\n3,Arnold,Three,55,male\n4,Krish Star,Four,60,female\n5,John Mike,Four,60,female\n6,Alex John,Four,55,male\n7,My John Rob,Fifth,78,male\n8,Asruid,Five,85,male\n9,Tes Qry,Six,78,male\n10,Big John,Four,55,female\n11,Ronald,Six,89,female\n12,Recky,Six,94,female\n13,Kty,Seven,88,female\n14,Bigy,Seven,88,female\n15,Tade Row,Four,88,male\n16,Gimmy,Four,88,male\n17,Tumyu,Six,54,male\n18,Honny,Five,75,male\n19,Tinny,Nine,18,male\n20,Jackly,Nine,65,female\n21,Babby John,Four,69,female\n22,Reggid,Seven,55,female\n23,Herod,Eight,79,male\n24,Tiddy Now,Seven,78,male\n25,Giff Tow,Seven,88,male\n26,Crelea,Seven,79,male\n27,Big Nose,Three,81,female\n28,Rojj Base,Seven,86,female\n29,Tess Played,Seven,55,male\n30,Reppy Red,Six,79,female\n31,Marry Toeey,Four,88,male\n32,Binn Rott,Seven,90,female\n33,Kenn Rein,Six,96,female\n34,Gain Toe,Seven,69,male\n35,Rows Noump,Six,88,female\n'
students = pd.read_csv(StringIO(student_csv))
students = students.rename(columns={"class": "class1", "gender": "sex"})
student_tsv = students.to_csv(sep="\t", index=False)
df = pd.read_table(StringIO(student_tsv))
print(df.head())
print("Rows:", len(df))
equivalent = pd.read_csv(StringIO(student_tsv), sep="\t")
assert equivalent.equals(df)Expected output
id name class1 mark sex
0 1 John Deo Four 75 female
1 2 Max Ruin Three 85 male
2 3 Arnold Three 55 male
3 4 Krish Star Four 60 female
4 5 John Mike Four 60 female
Rows: 35Use the actual file path. Relative paths use the current working directory. On Windows, a raw path such as r"D:\student.tsv" avoids accidental escape sequences. The notebook writes and reads a temporary file; see student sample data.
with tempfile.TemporaryDirectory(dir=Path.cwd()) as folder:
target = Path(folder) / "student.tsv"
target.write_text(student_tsv, encoding="utf-8")
loaded = pd.read_table(target, encoding="utf-8")
print("Same records:", loaded.equals(df))
assert loaded.equals(df)
# Local file: pd.read_table(r"D:\student.tsv")Expected output
Same records: Truesep overrides the default tab delimiter. Here the semicolon-separated text is embedded so the example needs no data.txt upload.
text = "Name;Age;ID\nRavi;30;001\nRaju;40;002\n"
people = pd.read_table(StringIO(text), sep=";", dtype={"ID": "string"})
print(people)
assert people["ID"].tolist() == ["001", "002"]Expected output
Name Age ID
0 Ravi 30 001
1 Raju 40 002skiprows=[0, 2] skips the first and third physical lines. In this sample they are a report title and an unwanted record. Keep the heading line or specify header/names explicitly.
text = "Practice report\nName\tAge\nIgnore\t99\nRavi\t30\nRaju\t40\n"
skipped = pd.read_table(StringIO(text), skiprows=[0, 2])
print(skipped)
assert skipped["Name"].tolist() == ["Ravi", "Raju"]Expected output
Name Age
0 Ravi 30
1 Raju 40usecols selects fields; its list order does not guarantee output order. Select columns again when a particular order matters. nrows limits data rows.
text = "ID\tName\tAge\n001\tRavi\t30\n002\tRaju\t40\n003\tAlex\t25\n"
selected = pd.read_table(StringIO(text), usecols=["Name", "Age"], nrows=2)
print(selected.loc[:, ["Name", "Age"]])
assert selected.shape == (2, 2)Expected output
Name Age
0 Ravi 30
1 Raju 40na_values adds markers when keep_default_na=True. With False, only your declared markers are treated as missing. Per-column rules help retain legitimate text such as the region code NA. See missing-value checks.
text = "Name\tAge\tRegion\nRavi\t30\tNA\nRaju\t?\tEU\nAlex\tNA\tNA\n"
missing = pd.read_table(StringIO(text), na_values={"Age": ["NA", "?"]}, keep_default_na=False)
print(missing)
assert missing["Age"].isna().sum() == 2
assert missing["Region"].tolist() == ["NA", "EU", "NA"]Expected output
Name Age Region
0 Ravi 30.0 NA
1 Raju NaN EU
2 Alex NaN NAFor known valid dates, use parse_dates and date_format. For mixed or invalid dates, retain text and apply to_datetime() with an explicit format and review policy.
text = "ID\tDate\n1\t2026-01-02\n2\t2026-01-03\n"
dated = pd.read_table(StringIO(text), parse_dates=["Date"], date_format="%Y-%m-%d")
print(dated)
assert dated.iloc[0]["Date"] == pd.Timestamp("2026-01-02")
raw_dates = pd.read_table(StringIO("ID\tDate\n1\t2026-01-02\n2\tbad\n"))
parsed = pd.to_datetime(raw_dates["Date"], format="%Y-%m-%d", errors="coerce")
print("Invalid date IDs:", raw_dates.loc[parsed.isna(), "ID"].tolist())Expected output
ID Date
0 1 2026-01-02
1 2 2026-01-03
Invalid date IDs: [2]index_col uses a field as row labels. IDs with leading zeros need a string dtype during reading. For later changes, use set_index() or reset_index().
text = "ID\tName\tAge\n001\tRavi\t30\n002\tRaju\t40\n"
indexed = pd.read_table(StringIO(text), index_col="ID", dtype={"ID": "string"})
print(indexed)
assert indexed.index.tolist() == ["001", "002"]Expected output
Name Age
ID
001 Ravi 30
002 Raju 40encoding controls file or binary-buffer decoding. A StringIO buffer is already text. Keep decoding errors visible so incorrect encoding can be diagnosed.
binary = BytesIO("Name\tAge\nJosé\t30\n".encode("utf-8-sig"))
decoded = pd.read_table(binary, encoding="utf-8-sig")
print(decoded)
assert decoded.iloc[0]["Name"] == "José"Expected output
Name Age
0 José 30The default error policy stops on lines with too many fields. on_bad_lines="skip" discards them, so compare counts and retain the source. This option does not validate numeric values; short rows may produce missing fields.
text = "ID\tAmount\n1\t5\n2\t8\textra\n3\t9\n"
try:
pd.read_table(StringIO(text))
except pd.errors.ParserError:
print("Extra field detected: default import stopped.")
skipped = pd.read_table(StringIO(text), on_bad_lines="skip")
print(skipped)
print("Kept rows:", len(skipped))
assert skipped["ID"].tolist() == [1, 3]Expected output
Extra field detected: default import stopped.
ID Amount
0 1 5
1 3 9
Kept rows: 2chunksize returns a reader yielding DataFrames. Accumulate results without retaining every chunk. Chunking limits each read; later operations still determine total memory use.
row_count = 0
mark_total = 0
with pd.read_table(StringIO(student_tsv), chunksize=3, usecols=["id", "mark"]) as reader:
for chunk in reader:
row_count += len(chunk)
mark_total += chunk["mark"].sum()
print("Rows:", row_count, "Marks:", mark_total)
assert row_count == len(students) and mark_total == students["mark"].sum()Expected output
Rows: 35 Marks: 2613Use string for identifiers and nullable Int64 for integer fields that may be missing. Invalid numeric text still needs validation. See dtype inspection and astype().
text = "ID\tName\tAge\n001\tRavi\t30\n002\tRaju\t\n"
typed = pd.read_table(StringIO(text), dtype={"ID": "string", "Age": "Int64"})
print(typed)
print(typed.dtypes)
assert typed["ID"].tolist() == ["001", "002"]
assert typed["Age"].isna().sum() == 1Expected output
ID Name Age
0 001 Ravi 30
1 002 Raju <NA>
ID string
Name str
Age Int64
dtype: objectUse header=None and names for a headerless file. When replacing an existing heading, add header=0; otherwise the old heading is read as a data record.
text = "001\tRavi\t30\n002\tRaju\t40\n"
headerless = pd.read_table(StringIO(text), header=None, names=["ID", "Name", "Age"], dtype={"ID": "string"})
print(headerless)
renamed = pd.read_table(StringIO(student_tsv), header=0,
names=["student_id", "student_name", "student_class", "score", "gender"])
print(renamed.head(2))
assert len(renamed) == len(students)Expected output
ID Name Age
0 001 Ravi 30
1 002 Raju 40
student_id student_name student_class score gender
0 1 John Deo Four 75 female
1 2 Max Ruin Three 85 maleA tab inside a properly quoted field belongs to that value. Splitting each line manually on tabs would misread it. Choose separator and quoting rules to match the producer.
text = 'ID\tNote\n1\t"blue\tpen"\n2\tplain\n'
quoted = pd.read_table(StringIO(text))
print("First note:", repr(quoted.iloc[0]["Note"]))
print("Shape:", quoted.shape)
assert quoted.iloc[0]["Note"] == "blue\tpen" and quoted.shape == (2, 2)Expected output
First note: 'blue\tpen'
Shape: (2, 2)For this synthetic report, require an ID, non-negative numeric amount and valid ISO date. Preserve raw records and retain rejected rows for review. Use the Data Cleaning hub for related checks.
sales_tsv = "ID\tDate\tAmount\n001\t2026-01-02\t5.25\n002\t2026-01-03\t20\n003\tbad\t8\n004\t2026-01-04\tpending\n"
raw = pd.read_table(StringIO(sales_tsv), dtype="string", keep_default_na=False)
clean = raw.copy()
clean["ID"] = raw["ID"].str.strip()
clean["Date"] = pd.to_datetime(raw["Date"], format="%Y-%m-%d", errors="coerce")
clean["Amount"] = pd.to_numeric(raw["Amount"], errors="coerce")
valid = clean["ID"].ne("") & clean["Date"].notna() & clean["Amount"].ge(0).fillna(False)
accepted = clean.loc[valid].copy()
review = raw.loc[~valid].copy()
print(accepted)
print("Review IDs:", review["ID"].tolist())
print("Accepted revenue:", accepted["Amount"].sum())
assert accepted["ID"].tolist() == ["001", "002"]
assert accepted["Amount"].sum() == 25.25
assert len(accepted) + len(review) == len(raw)Expected output
ID Date Amount
0 001 2026-01-02 5.25
1 002 2026-01-03 20.0
Review IDs: ['003', '004']
Accepted revenue: 25.25Pandas uses to_csv() with sep="\t" to write TSV. Use index=False to omit disposable row labels, then preserve ID types when reloading. See export options.
exported = accepted.to_csv(sep="\t", index=False, date_format="%Y-%m-%d")
print(exported.rstrip())
checked = pd.read_table(StringIO(exported), dtype={"ID": "string"})
assert checked["ID"].tolist() == ["001", "002"]
assert checked["Amount"].sum() == 25.25
# Optional Colab download:
# accepted.to_csv("accepted_sales.tsv", sep="\t", index=False, date_format="%Y-%m-%d")
# from google.colab import files
# files.download("accepted_sales.tsv")Expected output
ID Date Amount
001 2026-01-02 5.25
002 2026-01-03 20.0Is read_table() deprecated? It is supported in current Pandas. How does it differ from read_csv()? Its default separator is a tab rather than a comma. Does a .tsv extension force tab parsing? The separator setting controls parsing. Why did everything become one column? Check the delimiter. Why did zeros disappear from IDs? Declare a string dtype before inference. Does skipping bad lines clean invalid values? No; validate fields separately. Does chunksize return a DataFrame? It returns a reader yielding DataFrames. Can I read compressed text? Supported filename extensions allow compression inference. Can I parse space-separated text? Use an appropriate separator, such as a whitespace regex, when consistent with the file format.
Read only id, name and mark from student_tsv. Select students with marks at least 90 and export those records without an index.
Reload the exported result to check that the expected row count is retained.
scores = pd.read_table(StringIO(student_tsv), usecols=["id", "name", "mark"])
answer = scores.loc[scores["mark"].ge(90)]
answer_tsv = answer.to_csv(sep="\t", index=False)
print(answer_tsv.rstrip())
reloaded = pd.read_table(StringIO(answer_tsv))
assert len(reloaded) == int(scores["mark"].ge(90).sum())Expected output
id name mark
12 Recky 94
32 Binn Rott 90
33 Kenn Rein 96Open in Google Colab View on GitHub
Run the examples in order, change TSV values and import options, then inspect accepted records and review rows. Save your own copy to keep edits. All sample tables are included in the notebook.
Reference: Pandas read_table documentation.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.