Library / SDK
JakobGM/patito avatar
JakobGM/patito

patito makes a pydantic model double as a polars schema

A data modelling layer built on top of polars and pydantic

642 stars30 forksPythonMIT

At a glance

What is it?
patito is a MIT licensed library that lets a pydantic model serve as the schema for a polars data frame, which gives a team typed columns, readable validation errors and generated test data from one declaration, at the cost of a dependency on both libraries and a project that is not currently moving fast.
Who is it for?
patito fits a team that has already chosen polars and pydantic and wants one declaration to describe a tabular contract, validate frames at a service boundary and generate test fixtures that satisfy the contract. It does not fit a pandas-first codebase, since the documentation points you at pandera for that, and it does not fit a team that needs a stable library with a recent release trail.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 168 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One class, two jobs: the schema and the row

patito's central idea is small and consequential. You declare a pydantic model, and that class becomes the schema of a data frame, while instances of the same class represent single rows of that frame.

py
# models.py
from typing import Literal

import patito as pt


class Product(pt.Model):
    product_id: int = pt.Field(unique=True)
    temperature_zone: Literal["dry", "cold", "frozen"]
    is_for_sale: bool

That dual role is what removes the usual duplication. In a typical project the same fields are described twice, once as a schema for validation and once as a class for row-level access, and the two drift apart. Here there is one declaration, and the documentation states the intent plainly: the class represents the schema of the data frame, instances represent single rows.

The claimed uses follow from that. The documentation lists simple and performant data frame validation, easy generation of valid mock data frames for tests, retrieving and representing singular rows in an object-oriented manner, and providing a single source of truth for the core data models in a code base.

The project describes itself as combining pydantic and polars to write modern, type-annotated data frame logic, and it has first-class support for polars, which it quotes as a blazingly fast DataFrames library written in Rust.

The type annotation vocabulary and the Field options

The mapping from Python types to column types is the core of the design, and it is documented as a summary of dataframe-compatible type annotations.

Regular Python types such as int, float, bool, str and date are validated against compatible polars data types. Wrapping a type in typing.Optional indicates that the column accepts missing values. A field annotated with typing.Literal checks that only a restricted set of values is taken, either as the native dtype or as pl.Categorical.

On top of that, patito.Field carries the extra checks, and the list is longer than most validation libraries offer because each one is expressed as a polars operation rather than a Python predicate:

Field(dtype=...) pins a specific dtype where several are compatible with the annotated Python type, for example product_id: int = Field(dtype=pl.UInt32). Field(unique=True) checks that every row has a unique value. Field(gt=..., ge=..., le=..., lt=...) does bound checks for any combination of the four comparators. Field(multiple_of=divisor) requires every value to be a multiple of the divisor. Field(default=default_value, const=True) marks the column as required and requiring exactly that value.

String fields get Field(regex=...), Field(max_length=...) and Field(min_length=...), which the documentation says are validated with polars' efficient string processing. And Field(constraints=...) takes a single polars expression or a list of them, where every row must satisfy the constraints, for example even_field: int = pt.Field(constraints=pl.col("even_field") % 2 == 0).

The last one is the most useful and the most dangerous. Being able to write any polars expression means the validation is as expressive as polars itself, and it also means the semantics depend on the polars version you run.

Validation errors that tell you what is wrong

The failure path is as deliberate as the success path. patito raises a specific exception type, patito.exceptions.DataFrameValidationError, and the documented example shows the message it produces.

py
import polars as pl

df = pl.DataFrame(
    {
        "product_id": [1, 1, 3],
        "temperature_zone": ["dry", "dry", "oven"],
    }
)
try:
    Product.validate(df)
    
except pt.exceptions.DataFrameValidationError as exc:
    print(exc)

The printed output in the documentation lists three validation errors for Product, then one line per column with the detail. is_for_sale reports a missing column with the type error code type_error.missingcolumns, product_id reports two rows with duplicated values, and temperature_zone reports rows with invalid values, listing oven.

Two details make this output worth wanting. The errors are grouped per column rather than per row, which is the order a data engineer thinks in, and each entry carries a type code, so a test can assert on the category of failure rather than on a string that will change when the wording does. For a library whose job is to sit between a caller and a data contract, that reporting style is arguably the feature.

examples() exists because strict validation hurts tests

The documentation is unusually honest about the friction that strict validation creates, and the mock data generator is the answer to it.

The scenario is a function that validates its input before doing work:

py
import polars as pl

def num_products_for_sale(products: pl.DataFrame) -> int:
    Product.validate(products)
    return products.filter(pl.col("is_for_sale")).height

A test that calls it with a frame holding only is_for_sale will fail validation, and the fix is to add valid dummy values for temperature_zone and product_id in every test. The documentation argues that this quickly introduces a lot of boilerplate to all tests involving data frames, obscuring what is actually being tested in each test.

So patito provides the examples constructor, which generates test data that is fully compliant with the model schema:

py
Product.examples({"is_for_sale": [True, True, False]})

The documentation notes that the examples method accepts the same arguments as a regular data frame constructor, so a test can pin the columns it cares about and let the schema fill in the rest, which is a much better trade than hand-writing three valid rows in every test file.

The install line and what the packaging pulls in

Installation is a single pip command:

sh
pip install patito

The metadata behind it is where the real dependency story sits. The project requires Python 3.9 or later and depends on pydantic>=2.7.0, polars>=1.32.0 and typing-extensions, and it is built with hatchling. There are two optional extras, caching, which adds pyarrow>=5.0.0, and pandas.

The pandas extra is the interesting one, because the documentation's own advice is that patito supports pandas but is highly recommended to be used with polars, and it names pandera as the more feature-complete, pandas-first library. So the pandas path exists for compatibility rather than as a first-class target.

The development setup is modern and tells you what the maintainers care about. The dev group includes nox and nox-uv for task running, mypy and pyright for typing, ruff for linting, blackdoc for documentation formatting, pytest with pytest-cov and pytest-watcher, xdoctest for doctests, and uv for environment management, and the repository carries a uv.lock, a noxfile.py and a .pre-commit-config.yaml. The pytest configuration turns warnings into errors, with two explicit exceptions.

The authors are Jakob Gerhard Martinussen and Thomas Aarholt, and the documentation is on Read the Docs at patito.readthedocs.io.

The alternative, and why the pandas question is really a choice

The documentation names its own alternative, which is unusual and useful. For a much more feature-complete, pandas-first library, it points at pandera.

The difference in approach is the whole point. pandera is built around pandas, with a decorator or a class-based API that validates a DataFrame, and its feature set is deeper and more pandas-native, with a large catalogue of statistical checks. patito is built around polars, with the schema expressed as a pydantic model and the constraints expressed as polars expressions, which means the validation runs inside polars' own execution engine rather than row by row in Python.

That is a real performance difference, and it is also a real compatibility constraint. Code written for patito uses polars idioms, so a project that today validates with pandas and wants to stay on pandas should read the pandera documentation instead of porting.

A third path is to skip the validation layer and use polars' own schema and casting features directly, which costs nothing and catches the type errors but not the business rules, since a unique constraint, a bound check or a regex on a string column is not something a dtype expresses. The trade there is exactly the one patito is built around: type errors are cheap to catch, rule violations are not.

MIT terms, a 0.8 line, and a repository that has gone quiet

The licence is MIT, with the LICENSE file at the repository root and the same stated in the project metadata. The project URLs point at the GitHub repository under the JakobGM organisation and at the Read the Docs documentation.

The release trail is where a prospective adopter should look carefully. The recent releases are v0.8.6 published on 2026-02-04, v0.8.5 on 2025-09-02 and v0.8.4 on 2025-08-07, and the pyproject in the tree declares version 0.8.6. The repository is not archived, and the last push was on 2026-05-08, which is more than six months before the present date. There is no recent release to point at, so the project should be treated as one that is not currently receiving updates, whatever the badge in the README suggests.

That badge is worth a second look for a different reason. The CI and coverage badges in the README point at a repository under the kolonialno organisation rather than the JakobGM one, which suggests the project moved owners and the README was not fully updated. It is a small sign that the documentation lags the code, so treat the README and the Read the Docs site as the source for usage and the changelog for versions.

The rest of the repository is conventional and healthy for a library of this size: a src/ layout, a tests/ directory, a docs/ directory, the noxfile, the uv lock and a .tool-versions file pinning the tool versions the maintainers develop against.

Editorial conclusion

patito fits a team that has already chosen polars and pydantic and wants one declaration to describe a tabular contract, validate frames at a service boundary and generate test fixtures that satisfy the contract. It does not fit a pandas-first codebase, since the documentation points you at pandera for that, and it does not fit a team that needs a stable library with a recent release trail. Before adopting it, check the release dates against your roadmap, because the last published version is 0.8.6 from 2026-02-04 and the last push was on 2026-05-08, and confirm that the field constraints you need map onto polars expressions rather than pandas semantics.

Frequently asked questions

How do I install patito and what does it depend on?

The documented install command is pip install patito. The project requires Python 3.9 or later and depends on pydantic>=2.7.0, polars>=1.32.0 and typing-extensions, with optional extras for caching via pyarrow>=5.0.0 and for pandas.

How do I validate a polars data frame with patito?

Call validate on the model class, for example Product.validate(df). If the frame does not match the schema, it raises patito.exceptions.DataFrameValidationError with per-column messages that carry type codes such as type_error.missingcolumns.

How does patito generate valid test data?

The examples constructor builds a frame that satisfies the model schema, and it accepts the same arguments as a regular data frame constructor, so a test can pin the columns it cares about and let the rest be generated.

Is patito a good fit for a pandas project?

The documentation says patito supports pandas but is highly recommended to be used with polars, and it points at pandera as the more feature-complete, pandas-first library to look at instead.

How recent is the latest patito release?

The most recent release is v0.8.6 on 2026-02-04, before it v0.8.5 on 2025-09-02 and v0.8.4 on 2025-08-07, and the last push to the repository was on 2026-05-08. The project is released under the MIT Licence.

Official sources

  1. Issues
  2. JakobGM/patito on GitHub
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jakobgm-patito.svg)](https://hysenlabs.com/projects/jakobgm-patito)

Was this useful?