Skip to content
advanced Phase 18 · Python Performance

Optimization Patterns

Apply common Python optimizations — caching, slots, and algorithmic improvements.

1h
0 problems
Topic Progress 0%

Measure, Then Change Complexity

Measure, Then Change Complexity

The dominant win is almost always a better algorithm or data structure — in on a list is O(n) while dict/set lookup is O(1):

ids = {r.id for r in rows}                 # build once
selected = [r for r in rows if r.id in ids] # O(1) per probe

Benchmark the change honestly:

import timeit
timeit.timeit(lambda: lookup(values), number=100_000)

Before touching code, run cProfile and confirm the hotspot is where you think. Then classify the candidate: loop removal (algorithmic), caching repeated work (memoization), or micro-tweaks (local binding, comprehension) — only the first two usually move the needle, and all three must be re-measured afterward to prove the win survived.

Caching What Is Pure

Caching What Is Pure

from functools import lru_cache, cache, cached_property

@lru_cache(maxsize=256)          # bounded LRU eviction, 3.8+
def parse(expr: str) -> AST: ...

@cache                             # unbounded — 3.9+, use only for small domains
def fib(n: int) -> int: ...

class Config:
    @cached_property               # computed once per instance, stored in __dict__
    def parsed(self): return json.loads(self.raw)

Hazards to state before an interviewer nods:

  • Arguments must be hashable (no list/dict parameters)
  • typed=False (default) makes f(1) and f(1.0) collide because hash(1) == hash(1.0) — pass typed=True when that matters
  • Mutable arguments invalidate the premise — the cache keys on the value at call time, not later mutation
  • Unbounded cache on a wide input domain is a memory leak wearing a speedup costume

Structure-Level Micro-Optimizations

Structure-Level Micro-Optimizations

Once algorithms and caching are right, these measurements-backed tweaks help hot paths:

Change Why it wins
__slots__ on mass-instantiated classes skips per-instance __dict__ allocation and lookup
Bind hot globals/attributes to locals in a loop LOAD_FAST beats LOAD_GLOBAL/LOAD_ATTR per iteration
"".join(parts) over s += x strings are immutable — each += copies
Comprehensions over append loops fewer method lookups, tighter bytecode
collections.deque for head/tail pops list pop(0) shifts the whole array
defaultdict/set membership removes repeated lookups and setdefault noise

Guardrails: __slots__ forbids new attributes; premature local-binding hurts readability. Interview answer: state the profile evidence, the chosen lever, and the before/after number.

Quiz

1. Why is "x in values" slow for a large list but fast for a set?

Question 1 options

2. What does @lru_cache(typed=False) do with f(1) and f(1.0)?

Question 2 options

3. Which is NOT a benefit of __slots__?

Question 3 options

Flashcards

Question

lru_cache vs cache

Answer

lru_cache(maxsize) evicts by LRU; cache() is unbounded (3.9+) — safe only for small domains.

Question

Hashable arguments rule

Answer

Cache keys need hashability; lists/dicts as params raise TypeError.

Question

Optimization ladder

Answer

Profile → algorithm/data structure → caching → micro-tweaks → re-measure.

Question

deque vs list.pop(0)

Answer

deque pops at both ends O(1); list.pop(0) shifts every element O(n).

Revision Notes

Key Takeaways

  • 1. Profile first; asymptotic fixes dwarf micro-optimizations
  • 2. Caching requires pure functions, hashable args, and bounded domains
  • 3. Measure-verify every tweak and keep readability as a real cost in the ledger

Interview Tips

  • • Lead with evidence: name the profiler output that justified the change
  • • List lru_cache caveats (hashability, mutability, typed=False, unbounded growth) without prompting

Cheat Sheet

dict/set O(1) vs list O(n); @lru_cache(maxsize)/@cache; typed=False hash(1)==hash(1.0); slots cut dict; join not +=; measure with timeit.

Continue Learning