Memory and Performance
Measure representative work, identify allocation and algorithmic costs, and preserve correctness while optimizing.
By the end, you’ll be able to…
- Separate algorithmic growth from constant-factor changes.
- Use timeit, cProfile, and tracemalloc appropriately.
- Explain references, shallow sizes, and retention.
01Start with a workload and a budget
A useful performance question names an input distribution and a budget: importing fifty thousand rows within a latency limit, or keeping peak memory below a limit. An isolated fast expression is not evidence that an application improved. Record the interpreter, machine, input size, warm-up conditions, and correctness checks so another run can be compared meaningfully.
Look for repeated work before micro-optimizing syntax. Repeated membership tests against a list can scan the list for every query; a set can avoid that repeated scan at the cost of building an index and holding extra memory. For a single tiny lookup, building the set may cost more. Complexity describes growth, while measurement tells you where the tradeoff matters in your workload.
02Use the right measurement tool
timeit repeats a small operation and reduces some measurement noise. Keep setup outside the timed statement when setup is not part of the user-visible operation, and include it when it is. Repeat measurements rather than reporting one lucky run. Tiny timing differences can disappear under different hardware or interpreter versions, so do not turn sample timings into universal promises.
cProfile helps locate where a whole program spends time by recording call statistics. Profiling has overhead, so use it to select candidates and then remeasure the real workload. A high cumulative time can point to a frequently called parent rather than a slow individual line. Measure the effect of one change at a time and keep the simpler code if the improvement is insignificant.
from timeit import repeatitems = list(range(1000))lookup = set(items)assert (999 in items) == (999 in lookup)for label, statement in [('list', '999 in items'), ('set', '999 in lookup')]: samples = repeat(statement, globals=globals(), number=10000, repeat=3) print(label, 'best seconds:', min(samples))03References determine lifetime
Names refer to objects; assigning another name does not copy a list. Removing one name does not release an object if a cache, closure, traceback, or container still refers to it. CPython commonly uses reference counting with a cyclic garbage collector, but immediate destruction is not a portable resource-management contract. Use context managers to close files and sockets deterministically.
sys.getsizeof reports a shallow size and does not recursively total every referenced object. tracemalloc records traced Python allocations after tracing begins and can compare snapshots around an operation. It does not account for every byte held by native extensions or the operating system. Distinguish current retained memory, temporary peak memory, and process resident memory when reporting results; they answer different questions.
04Optimize with an explicit stopping point
Streaming can reduce peak memory when downstream code consumes one item at a time, but sorting or retaining every result removes that advantage. __slots__ may reduce per-instance overhead for suitable classes, yet restricts instance attributes and complicates some inheritance choices. Introduce it only after measuring a meaningful population of objects, not as a default badge of advanced Python.
Keep a regression check for the behavior that must remain equivalent, especially ordering, duplicate handling, and errors. Then compare realistic small and large inputs and report the tradeoff. When the budget is met, stop. Complexity in the implementation has a maintenance cost, and a faster implementation that loses validation or changes business rules is a correctness regression rather than a successful optimization.
The ideas worth keeping.
- Define a measurable budget before changing code.
- Profile to find candidates and benchmark to assess changes.
- Allocation tools and shallow sizes have explicit scope limits.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Remove repeated scanning
Count how many requested IDs are allowed. Build one lookup set, preserve duplicate requests, and verify the result against a straightforward implementation.
Explore a worked solution
def count_allowed(requested, allowed): index = set(allowed) return sum(item in index for item in requested)requested, allowed = [2, 2, 4, 9], [2, 4]expected = sum(item in allowed for item in requested)assert count_allowed(requested, allowed) == expected == 3The index is constructed once. Requests are not deduplicated, so repeated accepted requests still count.
Stream an aggregate
Compute the sum of squares below a limit without building a list of squares. Verify zero and a small positive limit.
Explore a worked solution
def sum_squares(limit): if limit < 0: raise ValueError('limit must be nonnegative') return sum(value * value for value in range(limit))assert sum_squares(0) == 0assert sum_squares(4) == 14The generator expression produces one square at a time. The final integer can still grow, but there is no retained list proportional to the number of inputs.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Remove repeated scanning
Count how many requested IDs are allowed. Build one lookup set, preserve duplicate requests, and verify the result against a straightforward implementation.
[Process exited with code 0 - No output]
Keep exploring.
The official references behind this chapter, when you want a second angle or the full details.
Pause. Put the pieces together.
You’ve reached the end of Python Internals and Protocols. Before moving on, use this task to check what you can do independently.
Demonstrate iterator consumption, preserve a wrapped function's metadata, and measure before proposing an optimization.
Try it in your own environment and write down the result. This is a self-check; reading completion does not evaluate your code.
Review this moduleOne chapter closer.
Reading is a start. Trying it yourself makes the connection.