Skip to content

peghaz/punctional

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Punctional

Functional programming for Python — monads, composable filters, and expressive data pipelines.

Python 3.12+ License: MIT

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

  1. Explicit over implicit — Errors are values, not exceptions
  2. Composability — Small, reusable units that combine easily
  3. Type safety — Generics provide IDE support and catch bugs early
  4. Immutability — Filters return new values, never mutate
  5. Zero dependencies — Pure Python, works everywhere

License

MIT License — see LICENSE for details.


Made with ❤️ for functional programming in Python

About

A functional programming framework for Python — enabling composable filters, method chaining, and expressive data pipelines.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages