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

CategoryFeatures
Functionsdef, lambda, closures, default arguments, *args, **kwargs
ClassesSingle and multiple inheritance, __init__, methods, properties, static methods, BFS MRO
Control flowif/elif/else, while, for x in, break, continue, return
ComprehensionsList [x for x in ...], dict {k:v for ...}, set {x for ...}, generator (x for ...)
String featuresf-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)
Mathmath.floor(), math.ceil(), math.sqrt(), math.pow(), abs(), min(), max(), sum(), range()
ExpressionsTernary x if cond else y, walrus :=, match/case, enumerate(), zip(), true division, ** power
Error handlingtry/except/finally with guard patterns (division, key, bounds)
Typesint (i64 + BigInt overflow), float (f64), bool, str, list, dict, set, None
Printprint() 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/except uses 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