Skip to main content

Vanilla-Python ergonomics on top of DSPy

Project description

FunnyDSPy [Experimental 🧪]

Vanilla-Python ergonomics on top of DSPy

I love DSPy, but I could not wrap my head around the fact that I have to write such a function-y Class which I could not use it as a normal Python function, with a 'normal' return value. So I wrote this library as an experiment to see if I could make it easier to use DSPy (for me!).

This library is a thin wrapper around DSPy that allows you to write plain functions and dataclasses and get DSPy modules and normal Python return values automatically. If you need the original DSPy Prediction (for optimization or loss computation), just call the function with _prediction=True.

Example 1:

import funnydspy as fd
import dspy

# Configure your language model
dspy.configure(lm=dspy.LM('openai/gpt-4.1-nano'))#, api_key='YOUR_OPENAI_API_KEY'))

@fd.ChainOfThought
def rag(query: str, context: str) -> str: return answer

# Get Python objects directly
answer = rag("What is the capital of France?", "France is a country in Europe.")
# → "The capital of France is Paris."

# Get DSPy Prediction for optimization
pred = rag("What is the capital of France?", "France is a country in Europe.", _prediction=True)
# → dspy.Prediction(reasoning="...", answer="The capital of France is Paris.")

Example 2:

import funnydspy as fd
import dspy
from typing import NamedTuple

# Configure your language model
dspy.configure(lm=dspy.LM('openai/gpt-4.1-nano'))

@fd.ChainOfThought
def analyze(numbers: list[float], threshold: float) -> tuple[float, list[float]]:
    """Analyze numbers and return statistics."""
    class Stats(NamedTuple):
        mean: float # The average of the numbers
        above: list[float] # Numbers above the threshold
    return Stats

# Get Python objects directly
mean_val, above_vals = analyze([1, 5, 3, 8, 2], 4.0)
# → (4.0, [5.0, 8.0])

# Get DSPy Prediction for optimization
pred = analyze([1, 5, 3, 8, 2], 4.0, _prediction=True)
# → dspy.Prediction(reasoning="...", mean=4.0, above=[5.0, 8.0])

✨ Features

  • 🐍 Pythonic: Write normal Python functions with type hints
  • 📦 Structured Returns: Use dataclasses, NamedTuples, or tuples for complex outputs
  • 🔄 DSPy Compatible: Seamlessly integrates with DSPy optimization and chaining
  • 📝 Smart Documentation: Extracts descriptions from docstrings and inline comments
  • 🎯 Type Safe: Automatic type conversion between LM strings and Python types
  • ⚡ Multiple Modules: Support for Predict, ChainOfThought, ReAct, and custom modules

📦 Installation

pip install funnydspy

🚀 Quick Start

Basic Usage with Dataclasses

from dataclasses import dataclass
from typing import List
import funnydspy as fd
import dspy

dspy.configure(lm=dspy.LM('openai/gpt-4.1-nano'))

@dataclass
class Stats:
    mean_value: float      # The average of all numbers
    above_threshold: List[float]  # Values greater than threshold

@fd.Predict
def analyze_data(numbers: List[float], threshold: float) -> Stats:
    """Analyze a list of numbers and return statistics."""
    return Stats

# Use it like a normal Python function
result = analyze_data([1, 5, 3, 8, 2], 4.0)
print(result.mean_value)        # 3.8
print(result.above_threshold)   # [5.0, 8.0]

Tuple Returns with Variable Names

@fd.ChainOfThought
def summarize_text(text: str) -> tuple[str, int, List[str]]:
    """Summarize text and extract key information."""
    summary = "A concise summary of the text"
    word_count = "Total number of words"
    key_points = "List of main points"
    return summary, word_count, key_points

summary, count, points = summarize_text("""Modules help you describe AI behavior as code, not strings.
To build reliable AI systems, you must iterate fast. But maintaining prompts makes that hard: it forces you to tinker with strings or data every time you change your LM, metrics, or pipeline. Having built over a dozen best-in-class compound LM systems since 2020, we learned this the hard way—and so built DSPy to decouple AI system design from messy incidental choices about specific LMs or prompting strategies.""")

Using Different DSPy Modules

# Chain of Thought reasoning
@fd.ChainOfThought
def complex_reasoning(problem: str) -> str:
    """Solve a complex problem step by step."""
    return solution

# Basic prediction
@fd.Predict
def simple_task(input_text: str) -> str:
    """Perform a simple text transformation."""
    return output

Working with Optimizers

# Access the underlying DSPy module for optimization
optimizer = dspy.BootstrapFewShot(metric=your_metric)
compiled_analyze = optimizer.compile(analyze_data.module, trainset=your_data)

# Wrap the optimized module back into a Pythonic interface
analyze_optimized = fd.funnier(compiled_analyze)

# Use the optimized version with the same interface
result = analyze_optimized([1, 5, 3, 8, 2], 4.0)

⚡ Elegant Parallel Execution

FunnyDSPy makes DSPy's parallel execution much more elegant and Pythonic with two approaches:

1. fd.parallel() - Direct parallel execution

Works only with FunnyDSPy decorated functions:

@fd.ChainOfThought
def analyze_text(text: str, context: str) -> str:
    """Analyze text in given context."""
    return analysis

# OLD VERBOSE DSPy SYNTAX:
# parallel = dspy.Parallel()
# pairs = [(analyze_text.module, {'text': t, 'context': ctx}) for t in texts]
# results = [pred.analysis for pred in parallel.forward(pairs)]

# ✨ NEW CLEAN FUNNYDSPY SYNTAX:
results = fd.parallel(analyze_text, [
    {'text': t, 'context': ctx} for t in texts
])

2. fd.parallelize() - DSPy-style parallelization (recommended)

Works with any function, giving you the same experience as standard DSPy:

@fd.ChainOfThought
def analyze_text(text: str, context: str) -> str:
    return analysis

# Create a parallelizable version
parallel_analyze = fd.parallelize(analyze_text)

# Use it like in standard DSPy
results = parallel_analyze([
    {'text': t, 'context': ctx} for t in texts
])

Key advantage: fd.parallelize() works with any function, including regular Python functions:

def regular_function(x: int, y: int) -> int:
    return x + y

# This works! (executes sequentially for non-DSPy functions)
parallel_func = fd.parallelize(regular_function)
results = parallel_func([{'x': 1, 'y': 2}, {'x': 3, 'y': 4}])  # [3, 7]

Real-world recursive example (just like standard DSPy):

def structure_and_summarize(parent_headings: List[str], chunks: List[str]) -> str:
    # ... base case ...
    
    # Parallelize DSPy functions
    produce_gist = fd.parallelize(gist_producer)
    chunk_gists = produce_gist([{'parent_headings': parent_headings, 'chunk': c} for c in chunks])
    
    # Even parallelize the recursive function itself!
    parallel_structure = fd.parallelize(structure_and_summarize)
    summarized_sections = parallel_structure([
        {'parent_headings': parent_headings + [prefix + topic], 'chunks': section_chunks}
        for topic, section_chunks in sections.items() if section_chunks
    ])
    
    return "\n\n".join([parent_headings[-1]] + summarized_sections)

Summary:

  • fd.parallel(func, inputs): Direct parallel execution (FunnyDSPy functions only)
  • fd.parallelize(func): Creates parallelizable version (any function, DSPy-style API)

📚 Documentation

Decorators

  • @fd.Predict - Basic prediction module
  • @fd.ChainOfThought - Chain of thought reasoning

Return Types

FunnyDSPy supports various return type patterns:

  1. Dataclasses: Structured data with field descriptions
  2. NamedTuples: Lightweight structured returns
  3. Tuples: Simple multiple returns with automatic field naming
  4. Primitives: Single values (str, int, float, etc.)

Type Conversion

FunnyDSPy automatically handles conversion between LM string outputs and Python types:

  • strstr (passthrough)
  • intint (parsed)
  • floatfloat (parsed)
  • boolbool (true/false/yes/no/1/0)
  • List[T]List[T] (JSON or comma-separated)
  • Dict[K, V]Dict[K, V] (JSON parsed)

Documentation Extraction

FunnyDSPy extracts field descriptions from multiple sources:

@dataclass
class Result:
    """Result container."""
    value: float  # The computed value
    status: str   # Processing status

@fd.Predict
def process(
    data: List[float],  # Input data to process
    mode: str          # Processing mode
) -> Result:
    """
    Process data and return results.
    
    Parameters
    ----------
    data: The input dataset
    mode: How to process the data
    
    Returns
    -------
    Result.value: The final computed value
    Result.status: Success or error status
    """
    return Result

🔧 Advanced Usage

Custom DSPy Modules

# Register custom DSPy modules
class CustomModule(dspy.Module):
    def __init__(self, signature):
        super().__init__()
        self.predictor = dspy.Predict(signature)
    
    def forward(self, **kwargs):
        return self.predictor(**kwargs)

fd.register(CustomModule, alias="custom")

@fd.custom
def my_function(input_text: str) -> str:
    return output

📄 License

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

🙏 Acknowledgments

  • Built on top of the excellent DSPy framework
  • Inspired by the need for more Pythonic LM programming interfaces

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

funnydspy-0.4.0.tar.gz (20.0 kB view details)

Uploaded Source

Built Distribution

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

funnydspy-0.4.0-py3-none-any.whl (15.7 kB view details)

Uploaded Python 3

File details

Details for the file funnydspy-0.4.0.tar.gz.

File metadata

  • Download URL: funnydspy-0.4.0.tar.gz
  • Upload date:
  • Size: 20.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.7

File hashes

Hashes for funnydspy-0.4.0.tar.gz
Algorithm Hash digest
SHA256 573dc53ff6bbb67f67943bef9036183c5fd2d330071ad9a13b2608deb47658b0
MD5 5cc7321e3dafdccc0d388e7e8c0bf07c
BLAKE2b-256 3242cda9a38c2d74d3e7ac76b964ddf6faa6769e41a144885e25d41dff93d300

See more details on using hashes here.

File details

Details for the file funnydspy-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: funnydspy-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 15.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.7

File hashes

Hashes for funnydspy-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a85130631162207fbc33a7c2868d1138075ca372d29f523f14195402a75956dd
MD5 0b3c7b55d147729c08d63f725592b09c
BLAKE2b-256 a40e8ec3326fa76ac982933c3a9f4f24e840d4c5606b54c5f48c7e877f5069f9

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