Intermediate

You have a function that processes a list of orders, applies discount rules, and calculates a final price. On most inputs it works perfectly — but one particular combination of values produces the wrong total, and you cannot figure out why. You add print() statements for every intermediate calculation, run the script, squint at the output, delete the prints, and repeat. Fifteen minutes later you realize the print you needed was inside a nested conditional you never thought to log. This is the debugging loop that birdseye was built to break.

birdseye is a Python debugger that works by recording the value of every single expression in a decorated function call, then serving a browser-based viewer where you can click through the execution interactively. You do not set breakpoints. You do not add print statements. You add one decorator, run your code, and then explore what actually happened at every step — including sub-expressions you never thought to check. It works with plain Python functions, methods, and scripts, and requires a single pip install.

This article covers everything you need to use birdseye effectively: installation, the @eye decorator, navigating the web viewer, capturing specific calls, debugging conditional logic and loops, and a real-life example using birdseye to track down a discount-calculation bug. By the end you will have a debugging technique that surfaces the exact value of every expression without a single print().

Python birdseye: Quick Example

Here is the fastest way to see birdseye in action. Install it, decorate a function with @eye, run it, and open the viewer in your browser.

# quick_birdseye.py
from birdseye import eye

@eye
def calculate_discount(price, quantity, member):
    base = price * quantity
    discount = 0.1 if member else 0.0
    if quantity >= 10:
        discount += 0.05
    total = base * (1 - discount)
    return total

# Call the function -- birdseye records every expression
result = calculate_discount(25.0, 12, True)
print(f"Total: {result}")

Output:

Total: 270.0

After running this script, birdseye has saved the full execution trace to a local SQLite database. Start the viewer with one command:

# In your terminal
python -m birdseye

Output:

Running on http://localhost:7777

Open http://localhost:7777 in your browser. You will see your function listed with a timestamp for each call. Click into a call and every expression in the function body — price * quantity, the member condition, discount + 0.05, the full base * (1 - discount) — is highlighted and shows its value when you hover or click. You never added a single log statement. birdseye captured everything automatically from the moment you applied the decorator.

What Is birdseye and Why Use It?

birdseye is an open-source Python debugging tool created by Alex Hall. Rather than pausing execution at a breakpoint like pdb or an IDE debugger, it lets your code run at full speed and records a complete trace of every function call. That trace is stored locally in a SQLite database named .birdseye.db in your working directory. The built-in web server reads that database and renders an interactive HTML view of your code where every expression is annotated with its runtime value.

The key mental model: birdseye is a flight recorder for your functions. The plane (your code) runs normally. After it lands (or crashes), you read the black box (the web viewer) to understand exactly what happened at every moment during the flight.

ToolHow It WorksBest ForInterrupts Execution?
print()Manual logging of specific valuesQuick one-off checksNo
pdbInteractive breakpoints, step-by-stepIsolated bug investigationYes — pauses code
IDE debuggerVisual breakpoints and watch expressionsGUI-driven step-throughYes — pauses code
loggingStructured log messages with levelsProduction observabilityNo
birdseyeRecords all expressions automaticallyComplex logic you can’t easily print-debugNo

birdseye’s advantage over print-debugging is completeness: it captures every sub-expression, not just the ones you thought to log. Its advantage over pdb is that it does not interrupt the run — your code executes normally, and you review the trace afterward. The trade-off is overhead: birdseye adds recording cost to every decorated function call, so it is a debugging tool, not a production profiler.

Installation and Setup

birdseye has two dependencies: the birdseye package itself and Flask, which it uses for the web viewer. Both install automatically with pip.

# Install birdseye
pip install birdseye

Output:

Successfully installed birdseye Flask Werkzeug ...

Verify the installation by checking the version:

# verify_install.py
import birdseye
print(birdseye.__version__)

Output:

0.9.5

birdseye supports Python 3.8 and above. It creates a .birdseye.db SQLite file in whatever directory you run your script from. You can point it at a different location by setting the environment variable BIRDSEYE_DB before running your script:

# Set a custom database path (bash)
export BIRDSEYE_DB=/tmp/myproject_debug.db
python my_script.py

# Or inline for a single run
BIRDSEYE_DB=/tmp/myproject_debug.db python my_script.py

This is useful when you are debugging a specific issue and want to keep the trace isolated from your normal debugging history, or when the script runs in a directory where you do not want to leave a database file.

Debug Dee at a glowing control panel recording expression values
Every sub-expression, recorded. No print() required.

The @eye Decorator

The @eye decorator is the only change you make to your code. Apply it to any function or method you want to trace. birdseye instruments the function’s bytecode at import time and wraps each call to record the trace.

# eye_decorator.py
from birdseye import eye

@eye
def process_order(items, tax_rate, promo_code):
    subtotal = sum(item["price"] * item["qty"] for item in items)
    discount = 0.15 if promo_code == "SAVE15" else 0.0
    taxable = subtotal * (1 - discount)
    tax = taxable * tax_rate
    total = taxable + tax
    return {"subtotal": subtotal, "discount": discount, "tax": tax, "total": total}

items = [
    {"price": 12.99, "qty": 3},
    {"price": 49.99, "qty": 1},
]

result = process_order(items, tax_rate=0.08, promo_code="SAVE15")
print(result)

Output:

{'subtotal': 88.96, 'discount': 0.15, 'tax': 6.041280000000001, 'total': 81.57528000000001}

When you open the viewer after running this, you can click the promo_code == "SAVE15" comparison and see it evaluated to True. You can click subtotal * (1 - discount) and see the intermediate value 75.616. The generator expression inside sum() is also recorded — you can see what each item["price"] * item["qty"] evaluated to for every item in the list.

You can also decorate methods in a class. The decorator works identically — just apply it to the method definition:

# eye_method.py
from birdseye import eye

class PricingEngine:
    def __init__(self, tax_rate):
        self.tax_rate = tax_rate

    @eye
    def calculate(self, price, quantity, member_discount):
        base = price * quantity
        discount = member_discount if member_discount else 0.0
        net = base - (base * discount)
        tax = net * self.tax_rate
        return round(net + tax, 2)

engine = PricingEngine(tax_rate=0.1)
print(engine.calculate(29.99, 4, 0.1))

Output:

107.96

In the viewer, the self.tax_rate attribute access is clickable and shows its value. The member_discount if member_discount else 0.0 conditional shows which branch was taken and what the result was. This is the real advantage of birdseye over a pdb breakpoint: you see every expression value, not just the ones you thought to inspect.

Navigating the birdseye Web Viewer

After running any code with @eye-decorated functions, start the viewer:

# Start the birdseye web server
python -m birdseye

Output:

Running on http://localhost:7777

The viewer has three main screens. The Calls list (/) shows every recorded call, grouped by function. You see the function name, file, and the time the call was made. Click any call to open it.

The Call detail view shows your function’s source code with every expression highlighted. Hover over any highlighted expression to see its value in a tooltip. Click an expression to pin the tooltip so it stays visible while you read the surrounding code. Expressions that raised exceptions are highlighted in red. Expressions that were not reached (inside an untaken branch) are greyed out — this immediately shows you which if branches ran and which did not.

The Iteration controls appear for any loop body. A set of arrow buttons lets you step through each iteration — so inside a for item in items loop, you can click forward and backward through iterations and see the value of every expression on each iteration individually. This replaces the classic pattern of adding print(f"iteration {i}: {item}") inside every loop you are debugging.

# iteration_demo.py
from birdseye import eye

@eye
def find_first_above_threshold(values, threshold):
    result = None
    for i, v in enumerate(values):
        scaled = v * 1.1          # apply a scaling factor
        if scaled > threshold:
            result = (i, scaled)
            break
    return result

data = [8.2, 9.5, 10.1, 7.8, 11.3]
print(find_first_above_threshold(data, threshold=10.5))

Output:

(2, 11.110000000000001)

In the viewer, you can step through iterations 0, 1, and 2 of the loop. On iteration 0, scaled is 9.02 and the if branch is not taken. On iteration 1, scaled is 10.45 — close, but still not above 10.5. On iteration 2, scaled is 11.11 and the branch fires. No manual print loop required.

Sudo Sam navigating interactive code branch map in the birdseye viewer
Clicked ‘if scaled > threshold’. Result: False. Iteration 1. Next.

Capturing Specific Calls

When a decorated function is called many times, the viewer fills up with hundreds of entries. You can filter which calls get recorded using the eye instance’s call options, or by using a separate eye instance configured for a specific context.

# selective_capture.py
from birdseye import eye

@eye
def validate_user_input(username, password):
    username_ok = len(username) >= 3 and username.isalnum()
    password_ok = len(password) >= 8
    has_digit = any(c.isdigit() for c in password)
    return username_ok and password_ok and has_digit

# Only some of these will be interesting to debug
test_cases = [
    ("alice", "secret123"),
    ("ab", "pass"),           # username too short, password too short
    ("bob123", "longenough"),  # valid username, no digit
    ("carol", "Abc12345"),     # should pass
]

for username, password in test_cases:
    result = validate_user_input(username, password)
    print(f"{username}: {result}")

Output:

alice: True
ab: False
bob123: False
carol: True

After running this, the viewer shows four separate call entries for validate_user_input. Click the call for ("ab", "pass") and you can see exactly which condition caused the False result: len(username) >= 3 evaluated to False (length was 2), so the short-circuit and never reached username.isalnum(). Click the call for ("bob123", "longenough") and you can see that password_ok was True (length is 10) but has_digit was False — every character in “longenough” failed c.isdigit(). These findings take 30 seconds to see in birdseye and would take minutes of print-debugging to reconstruct.

Debugging Conditional Logic and Nested Functions

birdseye is most valuable when the bug lives inside a nested condition or a multi-branch expression that is hard to print-debug. The following example has an intentional bug — a discount calculation that sometimes returns the wrong result.

# debug_discount.py
from birdseye import eye

@eye
def apply_pricing_rules(price, quantity, customer_tier, is_clearance):
    # Base discount from quantity
    if quantity >= 20:
        qty_discount = 0.15
    elif quantity >= 10:
        qty_discount = 0.08
    else:
        qty_discount = 0.0

    # Tier discount
    tier_discount = {"gold": 0.12, "silver": 0.06, "bronze": 0.02}.get(customer_tier, 0.0)

    # Clearance overrides tier discount (bug: should ADD, not replace)
    if is_clearance:
        final_discount = 0.25
    else:
        final_discount = qty_discount + tier_discount

    total = price * quantity * (1 - final_discount)
    return round(total, 2)

# Expected: clearance + gold + qty >= 10 should stack
print(apply_pricing_rules(50.0, 15, "gold", is_clearance=True))
# Expected: non-clearance gold + qty >= 10
print(apply_pricing_rules(50.0, 15, "gold", is_clearance=False))

Output:

562.5
494.0

In the viewer, click the clearance call. You can immediately see that final_discount was set to 0.25 flat — the if is_clearance branch replaced qty_discount + tier_discount entirely instead of stacking them. The tier_discount value (0.12) is visible, the qty_discount value (0.08) is visible, and the broken branch is highlighted red (well, taken — and you can see its output). The fix is one line: change final_discount = 0.25 to final_discount = qty_discount + tier_discount + 0.25. birdseye showed you the exact values that confirmed the diagnosis in under a minute.

Debug Dee pointing at highlighted conditional bug in code display
final_discount = 0.25. tier_discount = 0.12. Gone. That’s the bug.

Real-Life Example: Debugging an Order Pricing Pipeline

Here is a realistic pipeline that processes a batch of orders through multiple pricing rules. There are two bugs hidden in the code — birdseye will find both without a single added print statement.

# order_pipeline.py
from birdseye import eye

PRODUCT_CATALOG = {
    "A001": {"name": "Laptop Stand", "base_price": 39.99},
    "A002": {"name": "USB Hub", "base_price": 24.99},
    "A003": {"name": "Webcam", "base_price": 89.99},
}

TIER_DISCOUNTS = {"premium": 0.15, "standard": 0.05, "basic": 0.0}

@eye
def calculate_line_item(product_id, quantity, customer_tier):
    product = PRODUCT_CATALOG.get(product_id)
    if not product:
        return None

    unit_price = product["base_price"]
    tier_rate = TIER_DISCOUNTS.get(customer_tier, 0.0)

    # Bug 1: quantity discount check uses wrong threshold (should be >= 5)
    qty_bonus = 0.10 if quantity > 50 else 0.0

    discounted_price = unit_price * (1 - tier_rate - qty_bonus)
    line_total = discounted_price * quantity
    return round(line_total, 2)

@eye
def process_order(order):
    customer_tier = order.get("tier", "basic")
    line_items = []
    order_total = 0.0

    for item in order["items"]:
        line = calculate_line_item(item["sku"], item["qty"], customer_tier)
        if line is not None:
            line_items.append({"sku": item["sku"], "total": line})
            order_total += line

    # Bug 2: tax applied before checking if order qualifies (orders < $50 are tax-exempt)
    tax = order_total * 0.08
    final = order_total + tax

    return {
        "lines": line_items,
        "subtotal": round(order_total, 2),
        "tax": round(tax, 2),
        "final": round(final, 2),
    }

# Test orders
order_small = {
    "tier": "standard",
    "items": [{"sku": "A002", "qty": 1}],
}
order_bulk = {
    "tier": "premium",
    "items": [
        {"sku": "A001", "qty": 6},
        {"sku": "A003", "qty": 3},
    ],
}

print("Small order:", process_order(order_small))
print("Bulk order:", process_order(order_bulk))

Output:

Small order: {'lines': [{'sku': 'A002', 'total': 23.74}], 'subtotal': 23.74, 'tax': 1.9, 'final': 25.64}
Bulk order: {'lines': [{'sku': 'A001', 'total': 203.95}, {'sku': 'A003', 'total': 229.47}], 'subtotal': 433.42, 'tax': 34.67, 'final': 468.09}

Open birdseye and click the calculate_line_item call for the bulk order item with 6 units. In the viewer, quantity > 50 evaluates to False -- so the 6-unit order gets no quantity bonus even though the business rule should be 5+ units. The wrong threshold is instantly visible without any print statement. Click the process_order call for the small order: order_total is 23.74 and tax is 1.90, meaning the sub-$50 order is being taxed when it should not be. Both bugs are visible in the viewer in under two minutes of exploration. Fix them by changing quantity > 50 to quantity >= 5 and wrapping the tax block in if order_total >= 50.

Pyro Pete reviewing glowing order pipeline with expression value annotations
quantity > 50 returned False. qty was 6. Bug found.

Frequently Asked Questions

Does birdseye slow down my code significantly?

Yes -- birdseye adds recording overhead to every decorated function call, and the slowdown scales with the number of expressions in the function. For functions with simple math or conditionals, the overhead is typically 5x-20x slower than native execution. For tight loops with thousands of iterations, the overhead can be much higher. This is expected and intentional: birdseye is a debugging tool, not a production runtime. Never deploy decorated functions to production. Apply @eye only during a debugging session, remove or comment it out before any performance testing or deployment.

Where does birdseye store its data, and how do I clear it?

birdseye writes to .birdseye.db in the directory from which you run your script, unless you override the path with the BIRDSEYE_DB environment variable. Each run appends new call records to the existing database -- the file grows over time. To clear old data, delete the file and birdseye will create a fresh one on the next run: rm .birdseye.db. If you are debugging across many sessions, consider using a project-specific path with BIRDSEYE_DB so traces from different modules do not mix.

Does birdseye work with async functions?

birdseye has limited support for async functions. The @eye decorator can be applied to async def functions, but the recording behavior inside await expressions and concurrent coroutines is not as reliable as for synchronous functions. For async debugging, a combination of structured logging and Python 3.11+'s built-in asyncio.TaskGroup tracing is often more reliable. Check the birdseye GitHub repository for the current state of async support, as it has improved across releases.

How does birdseye handle large loops with thousands of iterations?

birdseye records data for every iteration, so a function with a 10,000-iteration loop will produce a very large trace entry in the database. The viewer will show all iterations with navigation arrows, but scrolling through thousands of iterations manually is impractical. A better approach is to reduce the input size during debugging -- run the function with a smaller sample (say, 10-20 items) that still reproduces the bug. Once you have identified the issue in the small sample, apply the fix and run the full dataset. Using BIRDSEYE_DB to isolate debugging sessions also keeps the database from growing unmanageably large.

Can I decorate all methods in a class at once?

There is no class-level decorator for birdseye -- you decorate individual methods. If you want to trace every method in a class without adding @eye to each one, you can write a simple class decorator that iterates over the class's methods and applies @eye to each one that is not a dunder method. However, this approach can produce a very large trace if the class has many methods with complex bodies, so prefer targeted decoration of the specific methods you are investigating.

Conclusion

This article covered the full birdseye workflow: installation, the @eye decorator for functions and methods, navigating the web viewer's call list and iteration controls, capturing specific call patterns, and using expression-level visibility to diagnose conditional logic bugs. The real-life example showed how birdseye surfaces two separate bugs -- a wrong threshold and a missing guard -- in a realistic order-processing pipeline, with no added print statements and under two minutes of viewer exploration.

The patterns in this article carry over directly to any Python debugging scenario where the bug lives in a complex expression, an untaken branch, or a loop iteration you did not think to check. Start with @eye on the function you suspect, run the failing input, and open the viewer. The correct value -- or the incorrect one -- will be waiting for you, annotated on every sub-expression. Extend the real-life example by decorating the entire pricing pipeline and adding more edge-case inputs to build a library of debug traces you can compare side by side.

For advanced birdseye usage including integration with test suites and custom database paths, see the official documentation at github.com/alexmojaki/birdseye.