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

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

Python 3.12+ License: MIT

🎯 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) and Result (Ok/Error) monads
  • 🔧 Extensible — Easily create custom filters for your domain
  • 📋 Dataclass Support — Make any dataclass functional with the Functional mixin

📦 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

  1. Immutability — Filters don't modify input; they return new values
  2. Composability — Filters can be combined to create complex transformations
  3. Type Safety — Generic types help catch errors at development time
  4. Readability — Pipe operator makes data flow explicit and easy to follow
  5. 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

punctional-0.1.0.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.0-py3-none-any.whl (12.7 kB view details)

Uploaded Python 3

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

Hashes for punctional-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d721c302727a98346501833682b731d82667336bebe202b4cdbde64c7c2077d7
MD5 4562aa3071d8c5a4918780435b0e9956
BLAKE2b-256 af65c48cdf17db7bd69998deb64d55baf5aee756739c21efaeff2ef42504b604

See more details on using hashes here.

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

Hashes for punctional-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 aec8175c49718066f1d0296fe3bc99704d8799f26970fae5e64940e0a2b0bf03
MD5 931f9180ed8f1fdfd7e986bbc4259643
BLAKE2b-256 9fb270a58fd97b224f2345c5a5ffd2102c403998e103e7b1b09e2bf89e28ee16

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