CHAPTER 41 · ADVANCED4 min read

Memory and Performance

Measure representative work, identify allocation and algorithmic costs, and preserve correctness while optimizing.

Python 3.12+Free to readModule 9 of 13
YOUR NEXT SMALL WINS

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.

membership.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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.

measure.pyPython
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.

allocation.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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.

LET IT CLICK

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.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

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.

01

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.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
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 == 3

The index is constructed once. Requests are not deduplicated, so repeated accepted requests still count.

02

Stream an aggregate

Compute the sum of squares below a limit without building a list of squares. Verify zero and a small positive limit.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
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) == 14

The 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.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01Why can a set-based lookup use more memory?
02What does a shallow size omit?
03A microbenchmark improves by 1%, but application timings overlap. What is justified?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

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.

EXPECTED OUTPUT
[Process exited with code 0 - No output]
challenge.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this chapter, when you want a second angle or the full details.

MODULE 09 · PRACTICAL SELF-CHECK

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 module

One chapter closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock chapter completion.