Python Energy Analysis
Joule provides comprehensive energy analysis for Python code. With 100+ runtime shims covering strings, lists, dicts, classes, comprehensions, and f-strings, most idiomatic Python runs unmodified.
Quick Start
# Static energy analysis (no execution)
joulec --lift python script.py
# Execute with energy tracking
joulec --lift-run python script.py
# Execute with energy optimization
joulec --energy-optimize --lift-run python script.py
# Generate JSON report for CI
joulec --lift python script.py --energy-report report.json
Supported Features
| Category | Features |
|---|---|
| Functions | def, lambda, closures, default arguments, *args, **kwargs |
| Classes | Single and multiple inheritance, __init__, methods, properties, static methods, BFS MRO |
| Control flow | if/elif/else, while, for x in, break, continue, return |
| Comprehensions | List [x for x in ...], dict {k:v for ...}, set {x for ...}, generator (x for ...) |
| String features | f-strings, .upper(), .lower(), .strip(), .split(), .replace(), .startswith(), .endswith(), .join(), .find(), .index(), + concatenation, * repetition (30+ methods) |
| List features | .append(), .pop(), .sort(), .reverse(), .index(), .count(), .copy(), slicing, len(), in operator (20+ methods) |
| Dict features | .get(), .pop(), .update(), .setdefault(), .keys(), .values(), .items(), in operator (15+ methods) |
| Math | math.floor(), math.ceil(), math.sqrt(), math.pow(), abs(), min(), max(), sum(), range() |
| Expressions | Ternary x if cond else y, walrus :=, match/case, enumerate(), zip(), true division, ** power |
| Error handling | try/except/finally with guard patterns (division, key, bounds) |
| Types | int (i64 + BigInt overflow), float (f64), bool, str, list, dict, set, None |
print() with end= parameter, polymorphic output (int/float/string) |
Common Energy Anti-Patterns
1. String Concatenation in Loops
# BAD — O(n^2) energy: each += allocates a new string
result = ""
for word in words:
result += word + " "
# GOOD — O(n) energy: join allocates once
result = " ".join(words)
Category: STRING | Severity: High | Savings: ~10x for large inputs
Each += on a string allocates a new buffer and copies the entire accumulated string. For 1,000 words averaging 5 characters, the bad version performs ~2.5 million character copies. The good version performs ~5,000.
2. Linear Search on List vs Set
# BAD — O(n) per lookup = O(n*m) total
for item in queries:
if item in large_list: # linear scan every time
process(item)
# GOOD — O(1) per lookup = O(n+m) total
lookup = set(large_list) # one-time O(n) cost
for item in queries:
if item in lookup: # hash lookup
process(item)
Category: DATA STRUCTURE | Severity: High | Savings: ~50x for 10K elements
3. Allocation Inside Hot Loops
# BAD — allocates a new list every iteration
for i in range(1000):
temp = []
temp.append(i)
process(temp)
# GOOD — reuse buffer
temp = []
for i in range(1000):
temp.clear()
temp.append(i)
process(temp)
Category: ALLOCATION | Severity: Medium | Savings: ~3x
4. Missing Early Exit
# BAD — always scans entire list
def find_first(items, target):
result = -1
for i in range(len(items)):
if items[i] == target:
result = i
return result
# GOOD — exits on first match
def find_first(items, target):
for i in range(len(items)):
if items[i] == target:
return i
return -1
Category: LOOP | Severity: Medium | Savings: ~2x average case
5. Recomputing Loop Invariants
# BAD — len(data) recomputed every iteration
for i in range(len(data)):
if i < len(data) - 1:
process(data[i], data[i + 1])
# GOOD — compute once
n = len(data)
for i in range(n - 1):
process(data[i], data[i + 1])
Category: REDUNDANCY | Severity: Low | Savings: ~1.2x
Worked Example
Given a data processing pipeline:
class DataProcessor:
def __init__(self, data):
self.data = data
self.results = []
def filter_positive(self):
filtered = []
for x in self.data:
if x > 0:
filtered.append(x)
self.data = filtered
def normalize(self):
total = sum(self.data)
self.data = [x / total for x in self.data]
def to_report(self):
report = ""
for i in range(len(self.data)):
report += f"Item {i}: {self.data[i]}\n"
return report
def main():
proc = DataProcessor([3.0, -1.0, 4.0, -2.0, 5.0, 1.0])
proc.filter_positive()
proc.normalize()
print(proc.to_report())
main()
Running energy analysis:
$ joulec --lift python pipeline.py
Energy Analysis: pipeline.py
DataProcessor____init__ 2.35 nJ (confidence: 0.95)
DataProcessor__filter_positive 8.72 nJ (confidence: 0.65)
DataProcessor__normalize 6.15 nJ (confidence: 0.70)
DataProcessor__to_report 14.80 nJ (confidence: 0.55)
main 3.20 nJ (confidence: 0.90)
Total: 35.22 nJ
Recommendations:
!! [STRING] DataProcessor__to_report — string concatenation in loop
Suggestion: use "".join() to build string in one allocation
Estimated savings: 8-10x for large inputs
! [REDUNDANCY] DataProcessor__to_report — len() called inside loop range
Suggestion: compute len() once before the loop
Estimated savings: 1.2x
JSON Energy Report
$ joulec --lift python pipeline.py --energy-report report.json
{
"source_file": "pipeline.py",
"language": "python",
"functions": [
{
"name": "DataProcessor__to_report",
"energy_pj": 14800,
"energy_human": "14.80 nJ",
"confidence": 0.55
}
],
"total_energy_pj": 35220,
"total_energy_human": "35.22 nJ",
"functions_lifted": 5,
"constructs_approximated": 2,
"recommendations": [
{
"function": "DataProcessor__to_report",
"category": "STRING",
"severity": "high",
"issue": "string concatenation in loop",
"suggestion": "use join() to build string in one allocation",
"savings_factor": 8.0
}
]
}
Energy Budget for CI
# Fail the build if total energy exceeds 50 nJ
$ joulec --lift python pipeline.py --energy-budget 50nJ
# Exit code 0: within budget
$ joulec --lift python pipeline.py --energy-budget 20nJ
# Exit code 1: budget exceeded (35.22 nJ > 20.00 nJ)
Limitations
- No external package imports (
import numpy,import requests, etc.) -- only built-in operations try/exceptuses guard patterns (division, key, bounds) rather than full exception semantics- Generator execution is approximated (constant iteration count estimate)
- No
async/await-- async patterns are desugared to synchronous equivalents - No decorator side effects -- decorators are recognized but not executed
- Class
__repr__,__str__,__eq__dunder methods are not auto-dispatched