A functional programming framework for Python — composable filters, method chaining, and expressive data pipelines with Option and Result monads.
Project description
Punctional
A functional programming framework for Python — enabling composable filters, method chaining, and expressive data pipelines.
🎯 What is Punctional?
Punctional is a lightweight functional programming library that brings powerful functional programming patterns to Python. It allows you to compose operations using an intuitive pipe (|) operator, create reusable transformation filters, and apply common functional design patterns like Option and Result monads.
Whether you're building data pipelines, validation logic, or just want cleaner, more declarative code — Punctional provides the building blocks.
✨ Key Features
- 🔗 Pipe Operator Chaining — Chain transformations using the intuitive
|operator - 🧱 Composable Filters — Create reusable, testable transformation units
- 📦 Functional Wrappers — Wrap native types (
int,float,str) for functional operations - 🎭 Monads — Built-in
Option(Some/Nothing) andResult(Ok/Error) monads - 🔧 Extensible — Easily create custom filters for your domain
- 📋 Dataclass Support — Make any dataclass functional with the
Functionalmixin
📦 Installation
pip install punctional
Or install from source:
git clone https://github.com/peghaz/punctional.git
cd punctional
pip install -e .
🚀 Quick Start
from punctional import fint, fstr, Add, Mult, ToUpper, GreaterThan, AndFilter, LessThan
# Arithmetic chaining
result = fint(10) | Add(5) | Mult(2) # (10 + 5) * 2 = 30
# String transformations
text = fstr("hello") | ToUpper() | Add("!") # "HELLO!"
# Logical validation
is_valid = fint(42) | AndFilter(GreaterThan(10), LessThan(100)) # True
📚 Core Concepts
Filters
A Filter is the fundamental building block — a transformation that takes an input and produces an output:
from punctional import Filter, fint
class Square(Filter[int, int]):
def apply(self, value: int) -> int:
return value ** 2
# Use it
result = fint(5) | Square() # 25
Functional Wrappers
Wrap native Python types to enable pipe operations:
| Function | Type | Description |
|---|---|---|
fint(x) |
FunctionalInt |
Functional integer wrapper |
ffloat(x) |
FunctionalFloat |
Functional float wrapper |
fstr(x) |
FunctionalStr |
Functional string wrapper |
The Pipe Operator
Chain filters using the | operator for readable, left-to-right transformations:
# Instead of nested calls:
result = Div(4).apply(Sub(3).apply(Mult(2).apply(Add(5).apply(10))))
# Write declaratively:
result = fint(10) | Add(5) | Mult(2) | Sub(3) | Div(4)
🔧 Built-in Filters
Arithmetic Filters
from punctional import 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 Filters
from punctional import GreaterThan, LessThan, Equals
fint(42) | GreaterThan(10) # True
fint(5) | LessThan(10) # True
fint(42) | Equals(42) # True
Logical Filters
from punctional import AndFilter, OrFilter, NotFilter, GreaterThan, LessThan, Equals
# AND: all conditions must be true
fint(42) | AndFilter(GreaterThan(10), LessThan(100)) # True
# OR: at least one condition must be true
fint(5) | OrFilter(LessThan(10), GreaterThan(100)) # True
# NOT: negate a condition
fint(5) | NotFilter(Equals(0)) # True
String Filters
from punctional import ToUpper, ToLower, Contains, fstr
fstr("hello") | ToUpper() # "HELLO"
fstr("WORLD") | ToLower() # "world"
fstr("hello world") | Contains("world") # True
fstr("ha") | Mult(3) # "hahaha"
List Filters
from punctional import Map, FilterList, Reduce, Mult, GreaterThan
numbers = [1, 2, 3, 4, 5]
# Transform each element
Map(Mult(2)).apply(numbers) # [2, 4, 6, 8, 10]
# Filter elements
FilterList(GreaterThan(2)).apply(numbers) # [3, 4, 5]
Composition
Create reusable pipelines with Compose:
from punctional import Compose, Mult, Add, fint
# Create a reusable transformation
double_plus_ten = Compose(Mult(2), Add(10))
fint(5) | double_plus_ten # 20
fint(10) | double_plus_ten # 30
🎭 Functional Design Patterns
Option Monad (Some/Nothing)
Handle nullable values without explicit None checks:
from punctional import Some, Nothing, some
# Wrap a value
value = Some(42)
print(value.map(lambda x: x * 2)) # Some(84)
print(value.get_or_else(0)) # 42
# Handle absence
empty = Nothing()
print(empty.map(lambda x: x * 2)) # Nothing
print(empty.get_or_else(0)) # 0
# Auto-convert from potentially None values
result = some(potentially_none_value) # Returns Nothing if None
Option Operations
| Method | Description |
|---|---|
map(f) |
Transform value if present |
flat_map(f) |
Chain operations returning Option |
bind(f) |
Alias for flat_map |
get_or_else(default) |
Get value or default |
get_or_none() |
Get value or None |
filter(predicate) |
Return Nothing if predicate fails |
or_else(alternative) |
Return alternative if Nothing |
is_some() |
Check if value is present |
is_nothing() |
Check if value is absent |
Result Monad (Ok/Error)
Handle operations that can fail with meaningful errors:
from punctional import Ok, Error, try_result
# Successful operation
success = Ok(42)
print(success.map(lambda x: x * 2)) # Ok(84)
# Failed operation
failure = Error("Something went wrong")
print(failure.map(lambda x: x * 2)) # Error("Something went wrong")
# Wrap potentially throwing functions
def divide(a, b):
return a / b
result = try_result(lambda: divide(10, 0))
# Error(ZeroDivisionError(...))
Result Operations
| Method | Description |
|---|---|
map(f) |
Transform success value |
map_error(f) |
Transform error value |
flat_map(f) |
Chain operations returning Result |
bind(f) |
Alias for flat_map |
get_or_else(default) |
Get value or default |
is_ok() |
Check if successful |
is_error() |
Check if failed |
to_option() |
Convert to Option (discards error info) |
🏗️ Extending the Framework
Creating Custom Filters
Simple Filter
from punctional import Filter
class Increment(Filter[int, int]):
def apply(self, value: int) -> int:
return value + 1
Parameterized Filter
class Power(Filter[int, int]):
def __init__(self, exponent: int):
self.exponent = exponent
def apply(self, value: int) -> int:
return value ** self.exponent
fint(2) | Power(3) # 8
Stateful Filter
class Accumulator(Filter[int, int]):
def __init__(self, initial: int = 0):
self.total = initial
def apply(self, value: int) -> int:
self.total += value
return self.total
acc = Accumulator()
acc(5) # 5
acc(10) # 15
acc(3) # 18
Functional Dataclasses
Make any dataclass functional with the Functional mixin:
from dataclasses import dataclass
from punctional import Functional, Filter
@dataclass
class Point(Functional):
x: float
y: float
class ScalePoint(Filter[Point, Point]):
def __init__(self, factor: float):
self.factor = factor
def apply(self, point: Point) -> Point:
return Point(point.x * self.factor, point.y * self.factor)
# Now Point supports piping!
point = Point(3, 4)
scaled = point | ScalePoint(2.5) # Point(7.5, 10.0)
📖 Examples
The examples/ directory contains comprehensive examples:
| File | Description |
|---|---|
| basics.py | Basic usage and introduction to all features |
| extending.py | Guide to creating custom filters and domain-specific extensions |
| data_transformation.py | Advanced patterns: validation pipelines, data transformations |
| quick_reference.py | Cheat sheet for quick lookup |
Run the examples:
python -m examples.basics
python -m examples.extending
python -m examples.data_transformation
🧪 Common Patterns
Validation Pipeline
from punctional import AndFilter, Functional, Filter
from dataclasses import dataclass
@dataclass
class Person(Functional):
name: str
age: int
email: str
class ValidateName(Filter[Person, bool]):
def apply(self, person: Person) -> bool:
return 1 <= len(person.name) <= 100
class ValidateAge(Filter[Person, bool]):
def apply(self, person: Person) -> bool:
return 0 <= person.age <= 150
class ValidateEmail(Filter[Person, bool]):
def apply(self, person: Person) -> bool:
return "@" in person.email and "." in person.email
# Use the validation pipeline
person = Person("Alice", 30, "alice@example.com")
is_valid = person | AndFilter(ValidateName(), ValidateAge(), ValidateEmail()) # True
Data Transformation Pipeline
class ApplyBonus(Filter[Person, Person]):
def __init__(self, percentage: float):
self.percentage = percentage
def apply(self, person: Person) -> Person:
return Person(person.name, person.age, person.email)
person | ApplyBonus(10) | PromoteAge() | SaveToDatabase()
List Processing Pipeline
from punctional import FilterList, Map, Mult
class IsEven(Filter[int, bool]):
def apply(self, value: int) -> bool:
return value % 2 == 0
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = FilterList(IsEven()).apply(numbers) # [2, 4, 6, 8, 10]
doubled = Map(Mult(2)).apply(result) # [4, 8, 12, 16, 20]
Error Handling with Result
from punctional import Result, Ok, Error
def fetch_user(id: int) -> Result[dict, str]:
if id < 0:
return Error("Invalid ID")
return Ok({"id": id, "name": "Alice"})
result = fetch_user(42).map(lambda u: u["name"]).get_or_else("Unknown") # "Alice"
result = fetch_user(-1).map(lambda u: u["name"]).get_or_else("Unknown") # "Unknown"
🎓 Design Principles
- Immutability — Filters don't modify input; they return new values
- Composability — Filters can be combined to create complex transformations
- Type Safety — Generic types help catch errors at development time
- Readability — Pipe operator makes data flow explicit and easy to follow
- Extensibility — Easy to create domain-specific filters
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
Made with ❤️ for functional programming enthusiasts
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
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 punctional-0.1.0.tar.gz.
File metadata
- Download URL: punctional-0.1.0.tar.gz
- Upload date:
- Size: 9.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d721c302727a98346501833682b731d82667336bebe202b4cdbde64c7c2077d7
|
|
| MD5 |
4562aa3071d8c5a4918780435b0e9956
|
|
| BLAKE2b-256 |
af65c48cdf17db7bd69998deb64d55baf5aee756739c21efaeff2ef42504b604
|
File details
Details for the file punctional-0.1.0-py3-none-any.whl.
File metadata
- Download URL: punctional-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aec8175c49718066f1d0296fe3bc99704d8799f26970fae5e64940e0a2b0bf03
|
|
| MD5 |
931f9180ed8f1fdfd7e986bbc4259643
|
|
| BLAKE2b-256 |
9fb270a58fd97b224f2345c5a5ffd2102c403998e103e7b1b09e2bf89e28ee16
|