Skip to main content

PyGraham 🚀

CI/CD Pipeline SonarQube Analysis PyPI version Python Versions License: MIT

A high-performance functional programming library for Python that brings truly functional programming capabilities with significant performance improvements over vanilla Python and other functional libraries.

Why PyGraham?

Python is wonderful, but it lacks true functional programming features. PyGraham fills this gap by providing:

  • 🎯 Type-safe monads (Maybe, Either) for elegant error handling
  • 📦 Immutable data structures with structural sharing
  • 🔗 Function composition (compose, pipe, curry)
  • 💤 Lazy evaluation for efficient data processing
  • 🎨 Pattern matching for expressive code
  • ⚡ C++ extensions for performance-critical operations

Installation

pip install pygraham

Quick Start

Elegant Error Handling with Monads

Before (Vanilla Python):

def process_data(data):
    if data is None:
        return None
    result = transform(data)
    if result is None:
        return None
    validated = validate(result)
    if validated is None:
        return None
    return save(validated)

After (PyGraham):

from pygraham import Maybe, pipe

process_data = pipe(
    Maybe.of,
    lambda m: m.flat_map(transform),
    lambda m: m.flat_map(validate),
    lambda m: m.flat_map(save)
)

# Or even cleaner:
result = (Maybe.of(data)
          .flat_map(transform)
          .flat_map(validate)
          .flat_map(save)
          .get_or_else(default_value))

Immutable Data Structures

from pygraham import ImmutableList, ImmutableDict

# Lists
numbers = ImmutableList.of(1, 2, 3, 4, 5)
doubled = numbers.map(lambda x: x * 2)
evens = numbers.filter(lambda x: x % 2 == 0)
total = numbers.reduce(lambda acc, x: acc + x, 0)

# Original unchanged!
assert list(numbers) == [1, 2, 3, 4, 5]

# Chaining operations
result = (ImmutableList.of(1, 2, 3, 4, 5)
          .filter(lambda x: x % 2 == 0)
          .map(lambda x: x * 2)
          .reverse())
# [8, 4]

# Dictionaries
config = ImmutableDict.of(debug=True, port=8080)
new_config = config.set("host", "localhost").set("debug", False)
# Original unchanged!

Function Composition

from pygraham import compose, pipe, curry

# Compose (right to left)
add_one = lambda x: x + 1
double = lambda x: x * 2
f = compose(double, add_one)
f(3)  # (3 + 1) * 2 = 8

# Pipe (left to right)
g = pipe(add_one, double)
g(3)  # (3 + 1) * 2 = 8

# Curry
@curry
def add_three(a, b, c):
    return a + b + c

add_three(1)(2)(3)  # 6
add_three(1, 2)(3)  # 6
add_three(1)(2, 3)  # 6

Lazy Evaluation

from pygraham import LazySequence

# Process infinite sequences efficiently
result = (LazySequence.infinite(1)
          .filter(lambda x: x % 2 == 0)
          .map(lambda x: x * 2)
          .take(5)
          .to_list())
# [4, 8, 12, 16, 20]

# Only computes what's needed!
large_data = LazySequence.from_iterable(range(1_000_000))
result = large_data.filter(lambda x: x % 100 == 0).take(10).to_list()
# Only processes 1000 elements, not 1 million!

Pattern Matching

from pygraham import match, case, _

def classify_number(n):
    return match(n,
        case(0, lambda x: "zero"),
        case(lambda x: x < 0, lambda x: "negative"),
        case(lambda x: x < 10, lambda x: "small"),
        case(lambda x: x < 100, lambda x: "medium"),
        case(_, lambda x: "large")
    )

classify_number(5)   # "small"
classify_number(50)  # "medium"
classify_number(500) # "large"

# Type-based matching
result = match("hello",
    case(int, lambda x: f"integer: {x}"),
    case(str, lambda x: f"string: {x}"),
    case(_, lambda x: "unknown")
)
# "string: hello"

Either Monad for Error Handling

from pygraham import Either, Left, Right

def divide(a, b):
    if b == 0:
        return Left("Division by zero")
    return Right(a / b)

result = (divide(10, 2)
          .map(lambda x: x * 2)
          .map(lambda x: x + 1)
          .fold(
              lambda error: f"Error: {error}",
              lambda value: f"Result: {value}"
          ))
# "Result: 11.0"

# Chaining operations
result = (Right(10)
          .flat_map(lambda x: divide(x, 2))
          .flat_map(lambda x: divide(x, 0))  # Error here
          .fold(
              lambda error: f"Error: {error}",
              lambda value: f"Result: {value}"
          ))
# "Error: Division by zero"

Performance Benchmarks

PyGraham includes C++ extensions for performance-critical operations, providing significant speedups over vanilla Python.

Data Processing Pipeline

Processing 10,000 transactions with filtering, mapping, and aggregation:

Implementation Time (ms) Speedup
Vanilla Python 2.45 1.0x
PyGraham 1.87 1.31x faster

List Operations

Processing lists with map, filter, reduce operations:

Operation Vanilla Python PyGraham Speedup
Map (100k items) 8.2ms 5.1ms 1.61x
Filter (100k items) 7.8ms 4.9ms 1.59x
Reduce (100k items) 6.5ms 3.8ms 1.71x

Complex Example: Travelling Salesman Problem

See examples/tsp_comparison.py for a complete before/after comparison.

Vanilla Python (Imperative):

def find_shortest_route_vanilla(cities):
    best_route = None
    best_distance = float('inf')

    for perm in permutations(range(len(cities))):
        route = list(perm)
        dist = calculate_distance(cities, route)
        if dist < best_distance:
            best_distance = dist
            best_route = route

    return best_route, best_distance

PyGraham (Functional):

def find_shortest_route_fp(cities):
    return (LazySequence.from_iterable(permutations(range(len(cities))))
            .map(lambda perm: ImmutableList(perm))
            .map(lambda route: (route, calculate_distance(cities, route)))
            .reduce(lambda best, current:
                    current if current[1] < best[1] else best,
                    (None, float('inf'))))

Results:

  • Cleaner code: Functional approach is more declarative
  • Immutable: No mutable state to track
  • Composable: Easy to modify and extend
  • Performance: Similar or better performance with lazy evaluation

Key Features in Detail

1. Maybe Monad

Handle optional values without null checks:

from pygraham import Maybe, Just, Nothing

# Safe dictionary access
def get_user_age(users, user_id):
    return (Maybe.of(users.get(user_id))
            .map(lambda user: user.get('age'))
            .filter(lambda age: age >= 0)
            .get_or_else(0))

# Safe computation chain
result = (Just(5)
          .map(lambda x: x * 2)
          .filter(lambda x: x > 8)
          .map(lambda x: x + 1)
          .get_or_else(0))
# 11

2. Immutable Collections

Efficient immutable data structures with structural sharing:

from pygraham import ImmutableList, ImmutableDict

# Lists support all functional operations
numbers = ImmutableList.of(1, 2, 3, 4, 5)
processed = (numbers
             .filter(lambda x: x % 2 == 0)
             .map(lambda x: x ** 2)
             .sort(reverse=True))

# Dictionaries are immutable too
user = ImmutableDict.of(name="Alice", age=30, city="NYC")
updated_user = user.set("age", 31).set("country", "USA")
# Original user unchanged

3. Lazy Evaluation

Process large datasets efficiently:

from pygraham import LazySequence

# Infinite sequences
fibonacci = (LazySequence.infinite(0)
             .scan(lambda acc, _: acc + 1, 0)
             .take(10)
             .to_list())

# Large file processing (only loads needed lines)
result = (LazySequence.from_iterable(open('huge_file.txt'))
          .filter(lambda line: 'ERROR' in line)
          .take(10)
          .to_list())

4. Pattern Matching

Expressive pattern matching for complex logic:

from pygraham import match, case, _, instance_of, in_range

def handle_response(response):
    return match(response.status_code,
        case(200, lambda _: "Success"),
        case(404, lambda _: "Not Found"),
        case(in_range(400, 499), lambda _: "Client Error"),
        case(in_range(500, 599), lambda _: "Server Error"),
        case(_, lambda code: f"Unknown: {code}")
    )

Examples

Check out the examples/ directory for comprehensive examples:

Basic Tutorials (Start Here!)

Learn each concept from the ground up with simple, clear examples:

Advanced Examples

Real-world comparisons showing functional programming advantages:

  • file_system_tree.py: File system tree traversal comparing OOP visitor pattern vs Functional recursion - showcases the elegance of recursive solutions, catamorphisms (fold), and pattern matching for hierarchical data
  • banking_transactions.py: Banking transaction processing comparing OOP vs Functional approaches - demonstrates the power of immutability, monads, and pure functions
  • tsp_comparison.py: Travelling Salesman Problem solved with vanilla Python vs PyGraham
  • data_pipeline.py: Complex data processing pipeline showcasing all features

Run examples:

# Basic tutorials
python examples/01_maybe_basics.py
python examples/02_either_basics.py
python examples/03_immutable_list_basics.py
python examples/04_compose_pipe_curry_basics.py
python examples/05_pattern_matching_basics.py

# Advanced examples
python examples/file_system_tree.py
python examples/banking_transactions.py
python examples/tsp_comparison.py
python examples/data_pipeline.py

Development

Setup

git clone https://github.com/Bernardi-sh/pygraham.git
cd pygraham
pip install -e ".[dev]"

Running Tests

pytest tests/ -v

Code Quality

PyGraham uses SonarQube for continuous code quality analysis. The project is analyzed for:

  • Code Quality: Bugs, vulnerabilities, code smells
  • Test Coverage: Line and branch coverage metrics
  • Security: Security hotspots and vulnerabilities
  • Maintainability: Technical debt and complexity

To set up SonarQube integration, see SONARQUBE_SETUP.md.

Building C++ Extensions

The C++ extensions are optional but provide significant performance improvements:

pip install pybind11
python setup.py build_ext --inplace

Why "PyGraham"?

Named in honor of Paul Graham, a pioneer in functional programming and the creator of Arc, who has advocated for the power of functional programming in software development.

Comparison with Other Libraries

Feature PyGraham fn.py toolz PyFunctional
Monads ✅ ❌ ❌ ❌
Immutable Collections ✅ ❌ ✅ ❌
Pattern Matching ✅ ❌ ❌ ❌
Lazy Evaluation ✅ ✅ ✅ ✅
C++ Extensions ✅ ❌ ✅ ❌
Type Hints ✅ ❌ ✅ ❌
Active Development ✅ ❌ ✅ ❌

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

  • Inspired by functional programming concepts from Haskell, Scala, and Clojure
  • Named after Paul Graham for his contributions to functional programming
  • Built with love for the Python community

Made with ❤️ for functional programming enthusiasts

Metadata

Release files for pygraham 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pygraham 0.1.0
File Size Uploaded
pygraham-0.1.0.tar.gz 23.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pygraham 0.1.0
File Interpreter ABI Platform
pygraham-0.1.0-py2.py3-none-any.whl Python 3, Python 2 none any Details

Total release size: 39.8 kB

Release files / pygraham-0.1.0.tar.gz

Download URL pygraham-0.1.0.tar.gz
Size 23.2 kB
Tags Source
SHA-256 checksum
How to use checksums
737ffd7372aae13b1e788ae1480b7e8f531114aa094f0e47bac6ec90adadc6fe
BLAKE2b-256 checksum
How to use checksums
5d7972636fcbcd1ffb0d1c012b0f69b021912325ff15207720dbbf2d3e66f330
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.14

Release files / pygraham-0.1.0-py2.py3-none-any.whl

Download URL pygraham-0.1.0-py2.py3-none-any.whl
Size 16.6 kB
Tags Python 2 Python 3
SHA-256 checksum
How to use checksums
6e9b7066844e2acc8c009419a81e0271182205cfc5de863b1ff197181896d38c
BLAKE2b-256 checksum
How to use checksums
2582d48ad3759077cae68f52ea8a2c2f2c8f424581043de772810f8d67f9ac2b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.14

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page