Functional programming for Python — monads, composable filters, and expressive data pipelines.
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.
pip install punctionalOr from source:
git clone https://github.com/peghaz/punctional.git
cd punctional
pip install -e .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) # NothingThe 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)) # 0Wrapping 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')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.0Functional 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 |
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.5Comparison:
from punctional import fint, GreaterThan, LessThan, Equals
fint(42) | GreaterThan(10) # True
fint(5) | LessThan(10) # True
fint(42) | Equals(42) # TrueLogical:
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)) # TrueString:
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]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)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) # 1024Add 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)| 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 |
| 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 |
| 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 |
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- 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
MIT License — see LICENSE for details.
Made with ❤️ for functional programming in Python