Skip to main content

A functional programming framework for Python — composable filters, method chaining, and expressive data pipelines with Option and Result monads.

Project description

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

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

punctional-0.1.2.tar.gz (9.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

punctional-0.1.2-py3-none-any.whl (12.6 kB view details)

Uploaded Python 3

File details

Details for the file punctional-0.1.2.tar.gz.

File metadata

  • Download URL: punctional-0.1.2.tar.gz
  • Upload date:
  • Size: 9.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.19

File hashes

Hashes for punctional-0.1.2.tar.gz
Algorithm Hash digest
SHA256 cbeccbfb38944990d2a390f7f0108a046383043425453076158616092bf99124
MD5 e4671f79e44e19e4247b7347eef1248b
BLAKE2b-256 f7ea732f6806de40d728dffe399937f968ecf6e0eb555b82c814cb9c5fb6fe2e

See more details on using hashes here.

File details

Details for the file punctional-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: punctional-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 12.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.19

File hashes

Hashes for punctional-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 a9ad8ed35de3584acadd06ecca48d3d74929effc7508d3ce3757123f2c7d32ff
MD5 73cf674ac205b6da03a4fe60c36b9711
BLAKE2b-256 834e22b8d00a93911a9ed530dfcef276743386e4277e8865b57f8f5f274590e7

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page