Skip to main content

Annotated-based refinement types for Python — with first-class Pydantic v2 integration. A practical projection of Σ-types into a language without dependent types.

Project description

refined-types

First-class refinement types for Python — with native Pydantic v2 integration. A practical projection of Σ-types into a language without dependent types.

Status: 0.1.0a2 — alpha. Core, predicates, and the Pydantic v2 adapter are live; Hypothesis strategy registration is a stub and lands in a later alpha. Not for production yet.

What it gives you

A Refined[T, P] value is a T that has been checked against predicate P at a parse boundary, with that evidence preserved by the type checker. Downstream code does not re-validate; it cannot accidentally accept an unrefined T either. The discipline is Σ-type-shaped: a smart constructor is the only way in, the witness is phantom (erased at runtime, distinct at the type level), and the refinement transports through function signatures.

Quickstart

from refined_types import (
    Err, NonEmptyString, Ok, Positive, Refined, parse, try_parse,
)

# 1. Smart-constructor flow — total handling via Result
match parse(Positive, 5):
    case Ok(value=qty):
        ...  # qty: Refined[int, Positive]
    case Err(error=e):
        print(e)  # value -1 failed predicate Positive: x > 0

# 2. Ergonomic flow when failure is a bug — raise on the spot
qty = try_parse(Positive, 5)        # qty: Refined[int, Positive]

# 3. Functions can demand refined inputs
def buy(qty: Refined[int, Positive], item: Refined[str, NonEmptyString]) -> None:
    ...

buy(qty, try_parse(NonEmptyString, "apple"))   # ok
buy(5, "apple")                                # type-checker rejects
#   ^^^ Argument of type "int" cannot be assigned to parameter "qty"
#       of type "Refined[int, Positive]" in function "buy"

The last call site is the load-bearing one: Refined[int, Positive] and plain int are distinct types under pyright strict and mypy strict in invariant positions. The library's main promise is that you cannot silently bypass the predicate by handing a function a raw value — that is enforced statically, before any code runs.

Pydantic v2 integration

Optional via pip install "refined-types[pydantic]". Importing the adapter installs schema-generation hooks; the core stays free of any pydantic import.

import refined_types.pydantic  # side-effect: install pydantic hooks
from pydantic import BaseModel
from refined_types import Email, NonEmptyString, Positive, Refined

class User(BaseModel):
    name:  Refined[str, NonEmptyString]
    age:   Refined[int, Positive]
    email: Refined[str, Email]

user = User.model_validate({"name": "Ada", "age": 30, "email": "a@b.co"})
# user.age is typed Refined[int, Positive], not int — see tests/test_pydantic_e2e.py

The emitted JSON Schema carries the predicate's constraints:

User.model_json_schema()["properties"]["age"]   # {"exclusiveMinimum": 0, ...}
User.model_json_schema()["properties"]["name"]  # {"minLength": 1, ...}

We instantiate via model_validate({...}) rather than User(name=..., age=30, ...) because the latter would require the caller to hand pydantic an unrefined int for a Refined[int, Positive] field — and the type checker correctly refuses. That refusal is the value prop, not a quirk.

Architecture

refined_types/
├── _core/          ← L1 + L2. Zero deps beyond typing_extensions.
├── pydantic/       ← L4 adapter. Optional via [pydantic] extra.
└── hypothesis/     ← L4 adapter stub. Strategy registration in a later alpha.

The core is framework-agnostic: it works with plain Python, dataclasses, attrs, msgspec, or any modeling layer. Adapters are isolated modules that import the core plus their framework and nothing else. A static test under tests/test_core_isolation.py enforces that _core/ never imports pydantic, hypothesis, attrs, or msgspec.

What ships in 0.1.0a2

  • Core: Result[T, E] (Ok/Err discriminated union with match / then / map / map_err), Refined[T, P] phantom marker, ParseError.
  • Predicates: Positive, Negative, NonNegative, NonPositive, Zero, NonEmptyString, MinLength.of(n), MaxLength.of(n), Email, NonEmptyCollection, Unique, Sorted. Twelve in total.
  • Parse: parse(P, value) -> Result[Refined[T, P], ParseError] and try_parse(P, value) -> Refined[T, P] (raises). Both ship on purpose — parse for total handling, try_parse for ergonomic call sites where a failure is a bug.
  • Pydantic v2 adapter (optional extra): refinement-aware __get_pydantic_core_schema__ + JSON Schema constraint emission.
  • Hypothesis adapter: stub that raises ImportError with install instructions. Strategy registration lands in a later alpha.

Why not phantom-types?

phantom-types is the incumbent and a good library. We differ on axes that matter for Pydantic-v2-native, type-checker-strict workflows:

Aspect phantom-types refined-types (0.1.0a2)
DSL inheritance Refined[T, P] generic
Core dependencies typeguard, numerary typing-extensions only
Pydantic v1 hook (legacy) v2 native, schema-aware
Type-checker distinctness weak (subclass of T) strong (distinct in invariant slot)
Parse failure raises only parse → Result and try_parse → raise
Predicate algebra functions: both(p,q) planned: operators P & Q (0.2.0)
Refined collections safe API absent planned (0.2.0)
Σ-type framing absent first-class in docs

Honest scope: anything marked "planned" is not in 0.1.0a2.

Σ-type framing — one paragraph

In MLTT/HoTT a Σ-type is Σ(x : T). P(x) — a value of T paired with a proof that P holds. Python has no dependent types, so we keep the shape of Σ rather than its full power: a smart constructor is the only entry, the witness is phantom (erased at runtime, distinct at the type level), and the refinement transports through function signatures. Cross-field dependent constraints, proof-relevant equality, type-level computation — none of that. The discipline Σ encourages — parse-don't- validate, illegal states unrepresentable, transport through types — is exactly what this library gives. See .context/SPEC.md §8 for the longer discussion.

Installation

# Core only — no pydantic, no hypothesis
pip install "refined-types==0.1.0a2"

# With Pydantic v2 integration
pip install "refined-types[pydantic]==0.1.0a1"

# Hypothesis extra installs hypothesis but the adapter is a stub for now
pip install "refined-types[hypothesis]==0.1.0a1"

# Everything
pip install "refined-types[all]==0.1.0a1"

Minimum Python: 3.10.

Roadmap

  • 0.1.0a1 — first alpha: core, 12 predicates, pydantic v2 adapter, type-check guards
  • 0.1.0a2 — docs alpha (you are here): honest README quickstart, type-checker rejection example, spec carrier rebaselined to drop the hypothesis overclaim
  • 0.1.0 (stable) — real Hypothesis strategy registration or explicit defer to 0.3.0, accumulated real-world feedback applied
  • 0.2.0 — predicate algebra (&, |, ~), refined collections with safe API (NonEmptyList.head() -> T)
  • 0.3.0 — Hypothesis strategy registration (if not in 0.1.0), FastAPI recipes, attrs/msgspec integration examples
  • 1.0.0 — stable API, full docs site

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

refined_types-0.1.0.tar.gz (17.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

refined_types-0.1.0-py3-none-any.whl (18.5 kB view details)

Uploaded Python 3

File details

Details for the file refined_types-0.1.0.tar.gz.

File metadata

  • Download URL: refined_types-0.1.0.tar.gz
  • Upload date:
  • Size: 17.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for refined_types-0.1.0.tar.gz
Algorithm Hash digest
SHA256 178c247446b55eb872dc27c2cfa8d9fbb8681d7a7839fd175a46e1c79610d669
MD5 912d72eec26a0e6e8dd19eb51588de8e
BLAKE2b-256 ad8bf3f3a172445f5cca1c0a6a2cd6126278a4c44d6bad77d69a74a9c5dae7ac

See more details on using hashes here.

Provenance

The following attestation bundles were made for refined_types-0.1.0.tar.gz:

Publisher: publish.yml on m0n0x41d/refined-types

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file refined_types-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: refined_types-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for refined_types-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a3649187393f9a88bd61a08308c2a9c8c5d2990b5391ac386b570e3259462b07
MD5 c8be5c09c6223b57fb70f961c444b376
BLAKE2b-256 95393c4a96913825f0a72e8d6ffb78a94b025619924421d67ea1bf570011c1df

See more details on using hashes here.

Provenance

The following attestation bundles were made for refined_types-0.1.0-py3-none-any.whl:

Publisher: publish.yml on m0n0x41d/refined-types

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page