Punctional
Functional programming for Python — monads, composable filters, and expressive data pipelines.
What is Punctional?
Punctional brings robust functional programming patterns to Python. It provides:
- Monads for safe error handling and null-safety (
Option,Result) - Composable filters that chain with the pipe (
|) operator - Functional wrappers for native types enabling method chaining
No dependencies. Pure Python. Type-safe.
Installation
pip install punctional
Or from source:
git clone https://github.com/peghaz/punctional.git
cd punctional
pip install -e .
Core Features
1. Option Monad — Safe Null Handling
The Option type represents a value that may or may not exist. Use Some to wrap a value and Nothing to represent absence — eliminating None checks and AttributeError exceptions.
from punctional import Some, Nothing, some
# Wrap existing values
user_name = Some("Alice")
print(user_name.map(str.upper)) # Some('ALICE')
print(user_name.get_or_else("Unknown")) # 'Alice'
# Handle missing values safely
missing = Nothing()
print(missing.map(str.upper)) # Nothing
print(missing.get_or_else("Unknown")) # 'Unknown'
# Auto-convert from potentially None values
def find_user(user_id):
return {"alice": "Alice"}.get(user_id)
result = some(find_user("bob")) # Nothing (because get returns None)
result = some(find_user("alice")) # Some('Alice')
Option Methods:
| Method | Description |
|---|---|
map(f) |
Transform the value if present |
flat_map(f) / bind(f) |
Chain operations that return Option |
filter(predicate) |
Return Nothing if predicate fails |
get_or_else(default) |
Get value or return default |
get_or_none() |
Get value or None |
or_else(alternative) |
Return alternative Option if Nothing |
is_some() / is_nothing() |
Check presence |
Chaining with Option:
from punctional import Some, Nothing
def parse_int(s: str) -> Option[int]:
try:
return Some(int(s))
except ValueError:
return Nothing()
def double(x: int) -> Option[int]:
return Some(x * 2)
# Chain operations safely
result = Some("42").flat_map(parse_int).flat_map(double) # Some(84)
result = Some("abc").flat_map(parse_int).flat_map(double) # Nothing
2. Result Monad — Explicit Error Handling
The Result type represents either success (Ok) or failure (Error). Unlike exceptions, errors are explicit in the type signature and must be handled.
from punctional import Ok, Error, try_result
# Explicit success and failure
success = Ok(42)
failure = Error("Something went wrong")
print(success.map(lambda x: x * 2)) # Ok(84)
print(failure.map(lambda x: x * 2)) # Error('Something went wrong')
print(success.get_or_else(0)) # 42
print(failure.get_or_else(0)) # 0
Wrapping exceptions with try_result:
from punctional import try_result
def divide(a, b):
return a / b
result = try_result(lambda: divide(10, 2)) # Ok(5.0)
result = try_result(lambda: divide(10, 0)) # Error(ZeroDivisionError(...))
# With custom error handler
result = try_result(
lambda: divide(10, 0),
error_handler=lambda e: f"Division failed: {e}"
) # Error('Division failed: division by zero')
Result Methods:
| Method | Description |
|---|---|
map(f) |
Transform success value |
map_error(f) |
Transform error value |
flat_map(f) / bind(f) |
Chain operations returning Result |
get_or_else(default) |
Get value or default |
is_ok() / is_error() |
Check success/failure |
to_option() |
Convert to Option (discards error info) |
Railway-oriented programming:
from punctional import Result, Ok, Error
def validate_age(age: int) -> Result[int, str]:
if age < 0:
return Error("Age cannot be negative")
if age > 150:
return Error("Age seems unrealistic")
return Ok(age)
def validate_name(name: str) -> Result[str, str]:
if not name.strip():
return Error("Name cannot be empty")
return Ok(name.strip())
# Chain validations
age_result = validate_age(25).map(lambda a: f"Age: {a}") # Ok('Age: 25')
age_result = validate_age(-5).map(lambda a: f"Age: {a}") # Error('Age cannot be negative')
3. Pipe Operator & Filters
Chain transformations using the | operator for readable, left-to-right data flow.
from punctional import fint, fstr, ffloat, Add, Mult, Sub, Div, ToUpper
# Arithmetic chaining
result = fint(10) | Add(5) | Mult(2) | Sub(3) # (10 + 5) * 2 - 3 = 27
# String transformations
text = fstr("hello") | ToUpper() | Add(" WORLD!") # 'HELLO WORLD!'
# Float operations
value = ffloat(10.0) | Div(4) | Mult(2) # 5.0
Functional Wrappers:
| Function | Type | Description |
|---|---|---|
fint(x) |
FunctionalInt |
Enables piping for integers |
ffloat(x) |
FunctionalFloat |
Enables piping for floats |
fstr(x) |
FunctionalStr |
Enables piping for strings |
4. Built-in Filters
Arithmetic:
from punctional import fint, Add, Sub, Mult, Div
fint(10) | Add(5) # 15
fint(10) | Sub(3) # 7
fint(10) | Mult(2) # 20
fint(10) | Div(4) # 2.5
Comparison:
from punctional import fint, GreaterThan, LessThan, Equals
fint(42) | GreaterThan(10) # True
fint(5) | LessThan(10) # True
fint(42) | Equals(42) # True
Logical:
from punctional import fint, AndFilter, OrFilter, NotFilter, GreaterThan, LessThan
# All conditions must pass
fint(42) | AndFilter(GreaterThan(10), LessThan(100)) # True
# At least one condition must pass
fint(5) | OrFilter(LessThan(3), GreaterThan(3)) # True
# Negate a condition
fint(5) | NotFilter(Equals(0)) # True
String:
from punctional import fstr, ToUpper, ToLower, Contains, Mult
fstr("hello") | ToUpper() # 'HELLO'
fstr("WORLD") | ToLower() # 'world'
fstr("hello world") | Contains("world") # True
fstr("ha") | Mult(3) # 'hahaha'
List Operations:
from punctional import Map, FilterList, Mult, GreaterThan
numbers = [1, 2, 3, 4, 5]
Map(Mult(2)).apply(numbers) # [2, 4, 6, 8, 10]
FilterList(GreaterThan(2)).apply(numbers) # [3, 4, 5]
5. Composition
Create reusable pipelines with Compose:
from punctional import Compose, Mult, Add, fint
# Define a reusable transformation
double_then_add_ten = Compose(Mult(2), Add(10))
fint(5) | double_then_add_ten # 20 (5 * 2 + 10)
fint(10) | double_then_add_ten # 30 (10 * 2 + 10)
6. Custom Filters
Create your own filters by extending the Filter base class:
from punctional import Filter, fint
class Square(Filter[int, int]):
def apply(self, value: int) -> int:
return value ** 2
class Power(Filter[int, int]):
def __init__(self, exponent: int):
self.exponent = exponent
def apply(self, value: int) -> int:
return value ** self.exponent
fint(5) | Square() # 25
fint(2) | Power(10) # 1024
7. Functional Dataclasses
Add the Functional mixin to any dataclass to enable piping:
from dataclasses import dataclass
from punctional import Functional, Filter
@dataclass
class Point(Functional):
x: float
y: float
class Scale(Filter[Point, Point]):
def __init__(self, factor: float):
self.factor = factor
def apply(self, p: Point) -> Point:
return Point(p.x * self.factor, p.y * self.factor)
class Translate(Filter[Point, Point]):
def __init__(self, dx: float, dy: float):
self.dx, self.dy = dx, dy
def apply(self, p: Point) -> Point:
return Point(p.x + self.dx, p.y + self.dy)
# Chain transformations on custom types
point = Point(3, 4)
result = point | Scale(2) | Translate(10, 10) # Point(16, 18)
Complete API Reference
Monads
| Type | Description |
|---|---|
Option[T] |
Abstract base for optional values |
Some(value) |
Contains a value |
Nothing() |
Represents absence |
some(value) |
Creates Some or Nothing based on None check |
Result[T, E] |
Abstract base for success/failure |
Ok(value) |
Successful result |
Error(error) |
Failed result |
try_result(fn) |
Wraps a function, catching exceptions as Error |
Filters
| Filter | Input → Output | Description |
|---|---|---|
Add(n) |
number → number |
Addition |
Sub(n) |
number → number |
Subtraction |
Mult(n) |
number → number |
Multiplication |
Div(n) |
number → number |
Division |
GreaterThan(n) |
number → bool |
Greater than comparison |
LessThan(n) |
number → bool |
Less than comparison |
Equals(n) |
any → bool |
Equality check |
AndFilter(*filters) |
T → bool |
Logical AND |
OrFilter(*filters) |
T → bool |
Logical OR |
NotFilter(filter) |
T → bool |
Logical NOT |
ToUpper() |
str → str |
Uppercase |
ToLower() |
str → str |
Lowercase |
Contains(s) |
str → bool |
Substring check |
Map(filter) |
list → list |
Apply filter to each element |
FilterList(pred) |
list → list |
Filter elements by predicate |
Compose(*filters) |
T → U |
Compose multiple filters |
Core Classes
| Class | Description |
|---|---|
Filter[T, U] |
Abstract base class for all filters |
Functional |
Mixin that enables | operator on any class |
FunctionalInt |
Wrapper for int with pipe support |
FunctionalFloat |
Wrapper for float with pipe support |
FunctionalStr |
Wrapper for str with pipe support |
Examples
Run the included examples:
python -m examples.basics # Introduction to all features
python -m examples.extending # Creating custom filters
python -m examples.data_transformation # Real-world patterns
python -m examples.quick_reference # Quick lookup cheatsheet
Design Principles
- Explicit over implicit — Errors are values, not exceptions
- Composability — Small, reusable units that combine easily
- Type safety — Generics provide IDE support and catch bugs early
- Immutability — Filters return new values, never mutate
- Zero dependencies — Pure Python, works everywhere
License
MIT License — see LICENSE for details.
Made with ❤️ for functional programming in Python
Metadata
Release files for punctional 0.2.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 | |
|---|---|---|---|
| punctional-0.2.0.tar.gz | 9.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| punctional-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 22.3 kB
Release files / punctional-0.2.0.tar.gz
| Download URL | punctional-0.2.0.tar.gz |
|---|---|
| Size | 9.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1e365ad77b6cff59950124033751266d01889c32a5fc7b6043655e95576ebd1c
|
|
BLAKE2b-256 checksum How to use checksums |
37d028c0b71d2d41c7a6ab2484dd768dc6109e38b7e62923226d15732729e7c1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.19
|
Release files / punctional-0.2.0-py3-none-any.whl
| Download URL | punctional-0.2.0-py3-none-any.whl |
|---|---|
| Size | 12.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9db1f1dd0a6f34106399324b9843eb932950badd2e54eb164143d6c6f6893e70
|
|
BLAKE2b-256 checksum How to use checksums |
fd45c2a6913b42aae74b48c4b3fc2815b7fc99aa57b6ae3bb965e320226570b7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.19
|