typeline
Write dataclasses to delimited text formats and read them back again.
Features type-safe parsing, optional field support, and an intuitive API for working with structured data.
Installation
The package can be installed with pip:
pip install typeline
Quickstart
Building a Test Dataclass
>>> from dataclasses import dataclass
>>>
>>> @dataclass
... class MyData:
... field1: int
... field2: str
... field3: float | None
Writing
>>> from tempfile import NamedTemporaryFile
>>> from typeline import TsvWriter
>>>
>>> temp_file = NamedTemporaryFile(mode="w+t", suffix=".tsv")
>>>
>>> with TsvWriter.from_path[MyData](temp_file.name) as writer:
... writer.write_header()
... writer.write(MyData(10, "test1", 0.2))
... writer.write(MyData(20, "test2", None))
Reading
>>> from typeline import TsvReader
>>>
>>> with TsvReader.from_path[MyData](temp_file.name) as reader:
... for record in reader:
... print(record)
MyData(field1=10, field2='test1', field3=0.2)
MyData(field1=20, field2='test2', field3=None)
Any Text Stream
To use an open text stream instead of a path, subscript the reader or writer class itself.
>>> import gzip
>>>
>>> with TsvWriter[MyData](gzip.open(f"{temp_file.name}.gz", "wt")) as writer:
... writer.write(MyData(10, "test1", 0.2))
>>>
>>> with TsvReader[MyData](gzip.open(f"{temp_file.name}.gz", "rt"), header=False) as reader:
... print(list(reader))
[MyData(field1=10, field2='test1', field3=0.2)]
Missing Values
None is written as an empty field.
When read, an empty field is None if the field allows None, and "" if it is a str.
Set none_field, e.g. to "NA", when an optional text field must tell "" and None apart.
Comments
A reader skips lines that start with any of its comment_prefixes, and hands each one to on_comment as a Comment with its line number.
A writer writes comments with write_comment, so comments can be passed straight from a reader to a writer and keep their places.
>>> _ = open(temp_file.name, "w").write("# made by a tool\nfield1\tfield2\tfield3\n10\ttest1\t0.2\n")
>>>
>>> with (
... TsvWriter.from_path[MyData](f"{temp_file.name}.copy") as writer,
... TsvReader.from_path[MyData](temp_file.name, comment_prefixes={"#"}, on_comment=writer.write_comment) as reader,
... ):
... writer.write_header()
... for record in reader:
... writer.write(record)
>>>
>>> print(open(f"{temp_file.name}.copy").read(), end="")
# made by a tool
field1 field2 field3
10 test1 0.2
Extra Columns
A record can end with an ExtraColumns field, which holds any columns past its other fields as text.
Readers fill it, writers write it back, and a header only needs to name the other fields.
>>> from typeline import ExtraColumns
>>>
>>> @dataclass
... class Region:
... name: str
... start: int
... extra: ExtraColumns = ()
>>>
>>> _ = open(temp_file.name, "w").write("exon1\t10\n\nexon2\t20\t0.9\tHIGH\n")
>>>
>>> with TsvReader.from_path[Region](temp_file.name, header=False) as reader:
... print(list(reader))
[Region(name='exon1', start=10, extra=()), Region(name='exon2', start=20, extra=('0.9', 'HIGH'))]
Custom Field Formats
Lists, dicts, sets, enums, and nested dataclasses are written as JSON by default.
For any other text format, a FieldCodec reads a field from its text and writes it back, chosen by the field's type.
Helpers in typeline.codecs cover common formats.
>>> from datetime import date
>>> from typeline import Codecs, FieldCodec
>>> from typeline.codecs import boolean, delimited
>>>
>>> @dataclass
... class Visit:
... patient: str
... seen: date
... consented: bool
... blocks: list[int]
>>>
>>> codecs: Codecs = {
... date: FieldCodec(from_text=date.fromisoformat, into_text=date.isoformat),
... bool: boolean(true="Y", false="N"),
... list[int]: delimited(int, sep=";"),
... }
>>>
>>> with TsvWriter.from_path[Visit](temp_file.name, codecs=codecs) as writer:
... writer.write(Visit("P-001", date(2026, 9, 29), True, [3, 1, 4]))
>>>
>>> print(open(temp_file.name).read(), end="")
P-001 2026-09-29 Y 3;1;4
>>>
>>> with TsvReader.from_path[Visit](temp_file.name, header=False, codecs=codecs) as reader:
... print(list(reader))
[Visit(patient='P-001', seen=datetime.date(2026, 9, 29), consented=True, blocks=[3, 1, 4])]
Custom types nested anywhere inside a field, like a list[Interval], are handled by enc_hook, which turns such an object into builtin values, and dec_hook, which builds it back from them.
Each hook raises NotImplementedError for types it does not handle.
>>> class Interval:
... def __init__(self, start: int, end: int) -> None:
... self.start = start
... self.end = end
...
... def __repr__(self) -> str:
... return f"Interval({self.start}, {self.end})"
>>>
>>> def enc_hook(obj: object) -> object:
... if isinstance(obj, Interval):
... return [obj.start, obj.end]
... raise NotImplementedError
>>>
>>> def dec_hook(kind: type, obj: object) -> object:
... if kind is Interval:
... return Interval(*obj)
... raise NotImplementedError
>>>
>>> @dataclass
... class Target:
... gene: str
... intervals: list[Interval]
>>>
>>> with TsvWriter.from_path[Target](temp_file.name, enc_hook=enc_hook) as writer:
... writer.write(Target("BRCA1", [Interval(1, 9), Interval(20, 25)]))
>>>
>>> print(open(temp_file.name).read(), end="")
BRCA1 [[1,9],[20,25]]
>>>
>>> with TsvReader.from_path[Target](temp_file.name, header=False, dec_hook=dec_hook) as reader:
... print(list(reader))
[Target(gene='BRCA1', intervals=[Interval(1, 9), Interval(20, 25)])]
Your Own Format
Subclass a reader to give a format its own defaults.
>>> from typing import TextIO
>>> from typing_extensions import Unpack
>>> from typeline import ReaderOptions, RecordType
>>> from typeline.codecs import key_value
>>>
>>> class VcfLikeReader(TsvReader[RecordType]):
... def __init__(self, handle: TextIO, /, **options: Unpack[ReaderOptions]) -> None:
... _ = options.setdefault("header", False)
... _ = options.setdefault("comment_prefixes", {"#"})
... _ = options.setdefault("none_field", ".")
... _ = options.setdefault("codecs", {dict[str, str]: key_value()})
... super().__init__(handle, **options)
>>>
>>> @dataclass
... class Site:
... chrom: str
... pos: int
... ident: str | None
... info: dict[str, str]
>>>
>>> _ = open(temp_file.name, "w").write("#CHROM\tPOS\tID\tINFO\nchr1\t100\t.\tDP=10;AF=0.5\n")
>>>
>>> with VcfLikeReader.from_path[Site](temp_file.name) as reader:
... print(list(reader))
[Site(chrom='chr1', pos=100, ident=None, info={'DP': '10', 'AF': '0.5'})]
Type checkers see VcfLikeReader.from_path[Site](...) as a TsvReader[Site], its closest built-in reader.
A reader fixed to one record type can add FixedRecordType to its bases, and is then built without a subscript.
>>> from typeline import FixedRecordType
>>>
>>> class SiteReader(VcfLikeReader[Site], FixedRecordType):
... pass
>>>
>>> with SiteReader.from_path(temp_file.name) as reader:
... print(list(reader))
[Site(chrom='chr1', pos=100, ident=None, info={'DP': '10', 'AF': '0.5'})]
More examples, from sample sheets to GFF3 and BED-like data, are in tests/test_real_world_examples.py.
Development and Testing
See the contributing guide for more information.
Metadata
Release files for typeline 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| typeline-1.1.0.tar.gz | 34.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| typeline-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 53.8 kB
Release files / typeline-1.1.0.tar.gz
| Download URL | typeline-1.1.0.tar.gz |
|---|---|
| Size | 34.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ff2d640b43409d8426b590b9372639f40694e6e0109c4c673744f461589e2d3e
|
|
BLAKE2b-256 checksum How to use checksums |
aaa0eb3e53ced02ffded86bfac689aedb45895eb807c18858099f3fd4337caf5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency logRelease files / typeline-1.1.0-py3-none-any.whl
| Download URL | typeline-1.1.0-py3-none-any.whl |
|---|---|
| Size | 19.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
14c68c434d3b55fe9b83b85aa6df41b5c4e048c768bb146b8434cc4cad1efa14
|
|
BLAKE2b-256 checksum How to use checksums |
96ecacdfb5568c93c123439b7b1fd53d34b36e878e28be79bb9f965ddb570162
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency log