pled – print-less debugging
pled is a library for debugging Python code so you don't have to paste print() everywhere.
It features no-code instrumentation on your codebase.
Use cases
- When you are writing a new function/module and want to trace its execution without
print-ing or logging everything yourself. - When you see a downstream exception but need to understand upstream code that may have caused it.
- When you need temporary state tracking which you will remove later.
- When you want to get a visual representation of the execution flow of your code.
Getting started
-
Install
pledas a library# With poetry poetry add --dev pled # With uv uv add --dev pled
-
Use an executor to execute code and collect traces into a tracer
# Tracing function `bar(*args, **kwargs)` inside module `foo` from pled import Executor executor = Executor("foo") tracer = executor.execute_function("bar", *args, **kwargs)
-
Inspect the traces in a tracer
# Print the traces print(tracer.format_traces()) # Or dump into stringified JSON json_traces = tracer.dump_json() # Or generate an HTML report (needs Internet connection as it uses mermaid.js from CDN) tracer.dump_report_file("report.html")
A sample output involving a your_project.main module and a your_project.dep module:
Click to expand JSON
[
{
"type": "FunctionEntry",
"timestamp": 0.010341791,
"function_name": "your_project.main.entry",
"args": [["flag", "2"]]
},
{
"type": "Branch",
"timestamp": 0.010348791,
"function_name": "your_project.main.entry",
"branch_type": "if",
"condition_expr": "flag > 0",
"evaluated_values": [["flag", "2"]],
"condition_result": true
},
{
"type": "FunctionEntry",
"timestamp": 0.010350833,
"function_name": "your_project.dep.dep_func",
"args": [["limit", "2"]]
},
{
"type": "Branch",
"timestamp": 0.01035325,
"function_name": "your_project.dep.dep_func",
"branch_type": "while",
"condition_expr": "i < limit",
"evaluated_values": [
["i", "0"],
["limit", "2"]
],
"condition_result": true
},
{
"type": "Branch",
"timestamp": 0.01035575,
"function_name": "your_project.dep.dep_func",
"branch_type": "while",
"condition_expr": "i < limit",
"evaluated_values": [
["i", "1"],
["limit", "2"]
],
"condition_result": true
},
{
"type": "Branch",
"timestamp": 0.010358625,
"function_name": "your_project.dep.dep_func",
"branch_type": "while",
"condition_expr": "i < limit",
"evaluated_values": [
["i", "2"],
["limit", "2"]
],
"condition_result": false
},
{
"type": "FunctionExit",
"timestamp": 0.010360333,
"function_name": "your_project.dep.dep_func",
"return_value": "3"
},
{
"type": "FunctionExit",
"timestamp": 0.010429666,
"function_name": "your_project.main.entry",
"return_value": null
}
]Types of traces
pled traces the following types of events:
FunctionEntry- function entryfunction_name- fully qualified function nameargs- the full argument list in name-value pairstimestamp- timestamp of the event
FunctionExit- function exitfunction_name- fully qualified function namereturn_value- return valuetimestamp- timestamp of the event
Branch- branchingfunction_name- fully qualified function name where the branch is locatedbranch_type- branch type, can beif,while, orexceptcondition_expr- condition expressionevaluated_values- evaluated valuescondition_result- condition resulttimestamp- timestamp of the event
Await- await expressionfunction_name- fully qualified function name where the await is locatedawait_expr- await expressionawait_value- value being awaitedawait_result- result of the awaittimestamp- timestamp of the event
Yield- yield expressionfunction_name- fully qualified function name where the yield is locatedyield_value- value being yieldedtimestamp- timestamp of the event
YieldResume- yield resumptionfunction_name- fully qualified function name where the yield is locatedsend_value- value being sent to the generatortimestamp- timestamp of the event
Note: timestamp is a float representing the number of seconds since the start of the
execution.
Examples
Tracing a module
Given this module:
# Module: your_project.main def just_print(): print("hello") just_print() # <-- this module does some work directly
You can trace the execution of this module by running:
from pled import Executor executor = Executor("your_project.main") tracer = executor.execute_module() print(tracer.format_traces())
Tracing a function
Given this module without root-level execution:
# Module: your_project.add def just_add(a: int, b: int) -> int: return a + b
You can trace the execution of a function by running:
from pled import Executor executor = Executor("your_project.add") tracer = executor.execute_function("just_add", 1, 2) print(tracer.format_traces())
Tracing multiple modules
You can trace multiple modules by passing a list of package or module names to the
Executor constructor.
Suppose you want to trace everything inside your_project package when executing
your_project.add.just_add function.
from pled import Executor executor = Executor("your_project.add", includes=["your_project"]) tracer = executor.execute_function("just_add", 1, 2) print(tracer.format_traces())
Tracing a function with background execution
You can run the executor in the background by setting the background option to True.
Given this module:
# Module: your_project.event_loop def infinite_yield(): import time while True: time.sleep(0.5) yield 1 def loop(): for _ in infinite_yield(): pass
You can run loop() in the background with:
from pled import Executor executor = Executor("your_project.event_loop", background=True) tracer = executor.execute_function("loop") while True: time.sleep(1) print(tracer.format_traces())
