Utility types for Python, such as `Result`, `Option`, etc.
Project description
📦 typeric
typeric is a practical type utility toolkit for Python, focused on clarity, safety, and ergonomics. It was originally built to make my own development experience smoother, but I hope it proves useful to others as well.
It currently provides lightweight, pattern-matchable types like Result and Option — inspired by Rust — with plans to include more common type patterns and error-handling abstractions.
pip install typeric
🚀 Features
-
🎯 Rust-style error propagation (the
?operator)
.spread()+@spreadableshort-circuits a function on the firstErr/NONE, exactly like Rust's?— the core of this library. Works for bothResultandOption, sync and async. -
✅ Functional-style
Resulttype
Ok(value)andErr(error)with powerful.map(),.and_then(),.combine(),.spread()helpers — inspired by Rust’sResult. -
🌀 Lightweight
Optiontype
Some(value)andNONEto handle nullable data safely, with.map(),.unwrap_or(),.is_some()and more. -
🔁 Seamless conversion decorators
@resulty: Wraps any function to returnResultinstead of raising exceptions.@optiony: Wraps any function to returnOption, convertingNoneor exceptions intoNONE.
-
🧩 Pattern matching support
Supports Python’smatchsyntax via__match_args__for bothOk/ErrandSome/NONE. -
🔒 Immutable and composable
Safe and clean method chains using.map(),.combine(),.inspect(), etc. -
🔧 Clean type signatures
Fully typed:Result[T, E]andOption[T]with static analysis and IDE support. -
🛠️ Extensible foundation
Designed for easy extension — more algebraic types (Either,Validated, etc.) can be added naturally.
🔍 Quick Example
Error propagation — Python's ? operator
The essence of Rust's error handling is not Result itself, but the ? operator:
fail fast, propagate the error, keep the happy path flat. typeric brings this to
Python with .spread() and @spreadable:
from typeric import Ok, Err, Result, spreadable
def find_user(uid: int) -> Result[str, str]:
return Ok("alice") if uid == 1 else Err("user not found")
def check_permission(user: str) -> Result[str, str]:
return Ok(user) if user == "alice" else Err("permission denied")
@spreadable
def handle_request(uid: int) -> Result[str, str]:
user = find_user(uid).spread() # like `find_user(uid)?` in Rust
granted = check_permission(user).spread()
return Ok(f"hello, {granted}")
handle_request(1) # Ok('hello, alice')
handle_request(42) # Err('user not found') — propagated, no if/else ladder
Option propagates the same way with @spreadable_option:
from typeric import Some, NONE, Option, spreadable_option
def maybe_positive(x: int) -> Option[int]:
return Some(x) if x > 0 else NONE
@spreadable_option
def add_one(x: int) -> Option[int]:
val = maybe_positive(x).spread() # NONE short-circuits the whole function
return Some(val + 1)
add_one(5) # Some(6)
add_one(-1) # NONE
Async versions: @spreadable_async and @spreadable_option_async.
Result
from typeric import Result, Ok, Err, resulty, resulty_async, spreadable
def parse_number(text: str) -> Result[int, str]:
try:
return Ok(int(text))
except ValueError:
return Err("Not a number")
match parse_number("42"):
case Ok(value):
print("Parsed:", value)
case Err(error):
print("Failed:", error)
# let function return Result[T,str]
@resulty
def add(x: int, y: int) -> int:
return x + y
res = add(1, 2)
if res.is_ok():
print("Result:", res.unwrap())
else:
print("Error:", res.err)
# let async function return Result[T,str]
@resulty_async
async def async_add(x: int, y: int) -> int:
return x + y
res = await async_add(1, 2)
if res.is_ok():
print("Result:", res.unwrap())
else:
print("Error:", res.err)
def func_a(x: int) -> Result[int, str]:
if x < 0:
return Err("negative input")
return Ok(x * 2)
@spreadable
def func_b(y: int) -> Result[int, str]:
a = func_a(y).spread()
return Ok(a + 1)
def test_func_b_success():
assert func_b(5) == Ok(11) # 5*2=10 +1=11
def test_func_b_propagate_error():
assert func_b(-2) == Err("negative input")
def validate_username(username: str) -> Result[str, str]:
if username.strip():
return Ok(username)
return Err("Username is empty")
def validate_age(age: int) -> Result[int, str]:
if age > 0:
return Ok(age)
return Err("Age must > 0")
def validate_email(email: str) -> Result[str, str]:
if "@" in email:
return Ok(email)
return Err("Invalid email")
# ✅ results combine
def validate_user_data(
username: str, age: int, email: str
) -> Result[tuple[tuple[str, int], str], str]:
return (
validate_username(username)
.combine(validate_age(age))
.combine(validate_email(email))
)
result1 = validate_user_data("alice", 30, "alice@example.com")
print(result1) # Ok((('alice', 30), 'alice@example.com'))
result2 = validate_user_data("", -5, "invalid-email")
print(result2.errs) # ['Username is empty', 'Age must > 0', 'Invalid email']
Option
from typeric import Option, Some, NONE, NoneType, optiony, optiony_async
from typeric.wrap_funcs import get_time_sync
def maybe_get(index: int, items: list[str]) -> Option[str]:
if 0 <= index < len(items):
return Some(items[index])
return NONE
match maybe_get(1, ["a", "b", "c"]):
case Some(value):
print("Got:", value)
case NoneType(): # note: `case NONE:` would be a capture pattern matching anything
print("Nothing found")
@get_time_sync # This decorator is used for synchronous functions to measure execution time.
@optiony
def get_number(x: int) -> int | None:
if x > 0:
return x
return None
@optiony_async
async def fetch_data(flag: bool) -> str | None:
if flag:
return "data"
return None
📖 API Reference
Imports
Everything is available from the top-level package:
from typeric import (
# Result
Result, Ok, Err,
# Option
Option, Some, NONE, NoneType,
# conversion decorators
resulty, resulty_async, optiony, optiony_async,
# propagation decorators (Rust's `?`)
spreadable, spreadable_async, spreadable_option, spreadable_option_async,
# errors & helpers
UnwrapError, NoneTypeError, EarlyReturn, AggregatedErrors,
)
Module paths also work: typeric.result, typeric.option, typeric.wrap_funcs.
Result[T, E] = Ok[T] | Err[E]
| Method | Ok(v) |
Err(e) |
|---|---|---|
.is_ok() / .is_err() |
True / False |
False / True |
.ok |
v |
— |
.err |
— | e |
.errs |
None |
all aggregated errors as list[E] |
.unwrap() |
v |
raises e if it is an exception, else UnwrapError |
.unwrap_or(default) |
v |
default |
.unwrap_or_else(f) |
v |
f(e) |
.map(f) |
Ok(f(v)) |
unchanged |
.map_err(f) |
unchanged | Err(f(e)), mapping all aggregated errors |
.and_then(f) |
f(v) (returns a Result) |
unchanged |
.or_else(f) |
unchanged | f(e) (returns a Result) |
.inspect(f) / .inspect_err(f) |
side effect on v / no-op |
no-op / side effect on e |
.combine(other) |
pair up values: Ok((v, other_v)) |
accumulate errors into .errs |
.spread() |
v |
short-circuit: propagate to nearest @spreadable |
Notes:
- Values are immutable —
combine,map_err, etc. always return new instances and never mutate the operands, soResultvalues can be safely reused and shared. Ok/Err(andSome/NONE) are hashable and usable in sets/dicts as long as the wrapped value is hashable.combineaccumulates: chain it across validations, then read every failure from.errs(first failure stays in.err).
Option[T] = Some[T] | NoneType
NONE is the singleton NoneType() instance.
| Method | Some(v) |
NONE |
|---|---|---|
.is_some() / .is_none() |
True / False |
False / True |
.unwrap() |
v |
raises NoneTypeError |
.unwrap_or(default) |
v |
default |
.map(f) |
Some(f(v)) |
NONE |
.and_then(f) |
f(v) (returns an Option) |
NONE |
.spread() |
v |
short-circuit: propagate to nearest @spreadable_option |
Pattern matching: use case Some(value): and case NoneType():.
(case NONE: is a capture pattern in Python — it matches anything and rebinds the name.)
Decorators
| Decorator | Applies to | Behavior |
|---|---|---|
@resulty |
sync def |
returns Ok(value); raised exceptions become Err(str(e)); a returned Result passes through with errors stringified |
@resulty_async |
async def |
same as @resulty |
@optiony |
sync def |
returns Some(value); None or a raised exception becomes NONE; a returned Option passes through |
@optiony_async |
async def |
same as @optiony |
@spreadable |
sync def returning Result |
catches .spread() short-circuits and returns them as Err |
@spreadable_async |
async def returning Result |
same as @spreadable |
@spreadable_option |
sync def returning Option |
catches .spread() short-circuits and returns NONE |
@spreadable_option_async |
async def returning Option |
same as @spreadable_option |
Propagation rules:
.spread()works by raising an internalEarlyReturnexception, caught by the nearestspreadable*wrapper — the function using.spread()must be decorated, otherwiseEarlyReturnescapes to the caller.@resulty/@optionydeliberately letEarlyReturnpass through, so they stack safely with the propagation decorators:
@spreadable # catches the propagation
@resulty # converts raised exceptions / bare returns
def handler(uid: int) -> int:
user = find_user(uid).spread() # Err propagates out past @resulty, intact
return len(user)
Timing decorators — typeric.wrap_funcs
@get_time_sync / @get_time_async log call arguments and execution time via the
standard-library logger named "typeric" (no third-party dependency). Enable output with:
import logging
logging.basicConfig(level=logging.INFO)
✅ Test
Run tests with:
uv run pytest -v
📦 Roadmap
- Async support (
resulty_async,optiony_async,spreadable_async,spreadable_option_async) - Rust-style
?propagation for bothResultandOption -
OptionResultcombinators -
Try,Either,NonEmptyList, etc.
📄 License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file typeric-0.2.0.tar.gz.
File metadata
- Download URL: typeric-0.2.0.tar.gz
- Upload date:
- Size: 35.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7239bfa395379507bc0509af7b338d3c9ae670493005b357408f2242b42dd3b8
|
|
| MD5 |
da5e781855507102dcb6f5adb3945520
|
|
| BLAKE2b-256 |
c91ce3c72ab49946f1ce1d1aaa10652df8e039a00df50acdd8c6020eb70f01c3
|
File details
Details for the file typeric-0.2.0-py3-none-any.whl.
File metadata
- Download URL: typeric-0.2.0-py3-none-any.whl
- Upload date:
- Size: 10.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8e9c7810b234ea8aa11b9ab1b851813f9f4d7592e9b18a66ca5990b69ae5d038
|
|
| MD5 |
914f255d22f043a3064c3459fa4ff9ae
|
|
| BLAKE2b-256 |
3c0b7551049d242c981456c4c50629d4070f2482052eab159bb6c9e7c321474b
|