Python 3 — SDE3 Reference
Pragmatic reference across DSA rounds, internals & trivia, gotchas, performance, production patterns, and mentoring juniors.
Collections & complexity
DSA list vs deque
list.pop(0) is O(n). Use collections.deque for O(1) popleft — critical for BFS.from collections import deque
q = deque([1, 2, 3])
q.appendleft(0) # O(1)
q.popleft() # O(1)
DSA heapq — min heap only
Python only has min-heap. For max-heap, negate values. heappush/heappop are O(log n).
import heapq
h = []
heapq.heappush(h, 3)
heapq.heappush(h, -5) # max-heap trick
heapq.heapify(lst) # O(n), in-place
heapq.nlargest(3, lst) # O(n log k)
DSA defaultdict & Counter
from collections import defaultdict, Counter
freq = Counter("abracadabra")
freq.most_common(2) # [('a',5),('b',2)]
graph = defaultdict(list)
graph[0].append(1) # no KeyError
DSA bisect — binary search
Sorted list ops in O(log n). Interview gold.
import bisect
a = [1, 3, 4, 7]
bisect.bisect_left(a, 4) # 2
bisect.bisect_right(a, 4) # 3
bisect.insort(a, 5) # keeps sorted
Sorting tricks
DSA sort stability & key
Python sort is Timsort — stable, O(n log n). Use
key= not cmp=.pairs = [(1,'b'), (2,'a'), (1,'a')]
pairs.sort(key=lambda x: (x[0], x[1]))
# custom comparator
import functools
functools.cmp_to_key(lambda a, b: a - b)
DSA set & frozenset
O(1) avg lookup. frozenset is hashable — use as dict key or in sets.
seen = set()
seen.add(1); 1 in seen # O(1)
a & b # intersection
a | b # union
a - b # difference
a ^ b # symmetric diff
Iteration patterns
DSA enumerate & zip
for i, v in enumerate(arr, start=1): ...
for a, b in zip(l1, l2): ...
# zip stops at shortest; use zip_longest
from itertools import zip_longest
DSA itertools essentials
from itertools import (
combinations, permutations,
product, accumulate, chain,
groupby, islice
)
list(combinations('ABC', 2))
list(accumulate([1,2,3])) # prefix sum
DSA two-pointer / sliding window
l, r = 0, 0
window = defaultdict(int)
while r < len(s):
window[s[r]] += 1
while invalid(window):
window[s[l]] -= 1
l += 1
r += 1
DSA sys.setrecursionlimit
Default is 1000. Deep DFS will hit it. Either raise it or convert to iterative with an explicit stack.
import sys
sys.setrecursionlimit(10**6)
# or better: iterative DFS
stack = [root]
while stack:
node = stack.pop()
...
Memory model
Trivia everything is an object
Integers, functions, classes — all PyObjects on the heap with refcount + type pointer. Small ints (-5 to 256) and interned strings are cached singletons.
a = 256; b = 256
a is b # True (cached singleton)
a = 257; b = 257
a is b # False (new object each time)
id(a) == id(b) # use == not is for values
Trivia GIL
Global Interpreter Lock — only one thread runs Python bytecode at a time. IO-bound: use threads. CPU-bound: use multiprocessing or C extensions. Python 3.13+ has experimental no-GIL mode.
from concurrent.futures import (
ThreadPoolExecutor, # IO-bound
ProcessPoolExecutor # CPU-bound
)
Trivia __slots__
Classes store instance attrs in a __dict__ by default. __slots__ eliminates the dict, saving ~40–50% memory per instance. Critical at scale.
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x, self.y = x, y
# no __dict__, no arbitrary attrs
Trivia generators & lazy eval
Generators are resumable functions. They yield one value at a time — O(1) memory vs O(n) for a list comprehension. Essential for streaming large data.
from itertools import islice
def chunked(iterable, n):
it = iter(iterable)
while chunk := list(islice(it, n)):
yield chunk
# generator expression — no allocation
total = sum(x*x for x in range(10**9))
Type system & data model
Trivia dunder methods
The data model — how Python operators map to methods.
__len__ __getitem__ __setitem__
__iter__ __next__ __contains__
__enter__ __exit__ # context manager
__hash__ __eq__ # if eq, must hash
__repr__ __str__ __format__
__call__ # callable object
Trivia MRO — C3 linearisation
Method Resolution Order for multiple inheritance. Use __mro__ to debug. super() follows MRO, not the parent class directly.
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
D.__mro__ # D → B → C → A → object
# Diamond problem solved
Trivia descriptor protocol
How @property, @classmethod, @staticmethod work under the hood. Implement __get__/__set__/__delete__ to make your own.
class Validator:
def __set_name__(self, owner, name):
self.name = name
def __set__(self, obj, val):
if val < 0: raise ValueError
obj.__dict__[self.name] = val
Trivia walrus operator :=
Assignment expression (3.8+). Assign and test in one go. Avoids double-calling expensive functions.
# clean while loops
while chunk := f.read(8192):
process(chunk)
# filter + transform in one pass
results = [y for x in data
if (y := expensive(x)) > 0]
Classic interview traps
Gotcha mutable default argument
Default args are evaluated once at definition time, not per call. Shared across all calls — silent mutation bug.
# WRONG
def append(val, lst=[]):
lst.append(val); return lst
# RIGHT
def append(val, lst=None):
if lst is None: lst = []
lst.append(val); return lst
Gotcha late binding closures
Closures capture variable by reference, not value. The classic loop lambda trap.
# WRONG — all return 9
fns = [lambda: i for i in range(10)]
# RIGHT — capture by value
fns = [lambda i=i: i for i in range(10)]
Gotcha == vs is
is checks identity (same object). == checks equality. Only use is for None, True, False singletons.x = [1,2,3]; y = [1,2,3]
x == y # True (same value)
x is y # False (different objects)
# correct None check
if val is None: ...
Gotcha shallow vs deep copy
import copy
a = [[1,2], [3,4]]
b = a[:] # shallow — inner lists shared
c = a.copy() # also shallow
d = copy.deepcopy(a) # fully independent
b[0].append(9) # mutates a[0] too!
Gotcha float precision
0.1 + 0.2 == 0.3 # False!
import math
math.isclose(0.1 + 0.2, 0.3) # True
from decimal import Decimal
Decimal('0.1') + Decimal('0.2') # exact
Gotcha dict mutation during iteration
Dicts are insertion-ordered since 3.7. Never mutate a dict/list while iterating over it.
# safe: iterate a copy
for k in list(d.keys()):
if condition(k): del d[k]
# or use a comprehension
d = {k: v for k, v in d.items()
if not condition(k)}
Profile first, optimise second
Perf profiling tools
import timeit
timeit.timeit("'-'.join(map(str,range(100)))", number=10000)
# line profiler (CLI)
# python -m cProfile -s cumtime script.py
from tracemalloc import start, take_snapshot
Perf list comp > map > loop
List comprehensions run in a C loop, faster than explicit for. But generators beat both for large data — no allocation.
# fastest for small-medium
[x*x for x in range(n)]
# fastest for large (lazy)
(x*x for x in range(n))
# string join pattern
''.join([str(x) for x in lst])
Perf local var lookup
Python looks up LEGB (Local → Enclosing → Global → Builtin). Localising a global in hot loops gives ~20% speedup.
def hot_loop(data):
_append = result.append # local ref
_sqrt = math.sqrt # local ref
for x in data:
_append(_sqrt(x)) # faster
Perf functools.lru_cache
Memoisation with one decorator. Use maxsize=None for unbounded. @cache is the 3.9+ alias.
from functools import lru_cache, cache
@cache # 3.9+ unbounded memo
def fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)
fib.cache_info() # hits/misses/size
Perf numpy / array over lists
For numerical work, numpy arrays are 10–100× faster — vectorised C ops, no boxing overhead.
import numpy as np
a = np.array([1,2,3], dtype=np.int32)
a * 2 # vectorised, no loop
np.sum(a) # C speed sum
a[a > 1] # boolean indexing
Perf asyncio for IO-bound work
asyncio for concurrent IO without threads. Key for services making many external calls.
import asyncio
async def fetch_all(urls):
async with aiohttp.ClientSession() as s:
tasks = [fetch(s, u) for u in urls]
return await asyncio.gather(*tasks)
Production-grade Python
Prod dataclasses & pydantic
from dataclasses import dataclass, field
@dataclass(frozen=True) # immutable
class Config:
host: str
port: int = 8080
tags: list = field(default_factory=list)
# pydantic for runtime validation
from pydantic import BaseModel, validator
Prod context managers
from contextlib import contextmanager, suppress
@contextmanager
def timer(label):
t = time.perf_counter()
yield
print(f"{label}: {time.perf_counter()-t:.3f}s")
with suppress(FileNotFoundError):
os.remove(tmp_file) # no try/except
Prod pathlib over os.path
from pathlib import Path
p = Path("data/input.csv")
p.exists(); p.suffix # '.csv'
p.parent / "output.csv" # path join
p.read_text(encoding="utf-8")
list(Path(".").glob("**/*.py"))
Prod typing essentials
from typing import (
Optional, Union, Any,
TypeVar, Generic, Protocol,
overload, TypedDict, TYPE_CHECKING
)
# 3.10+: use X | Y instead of Union[X,Y]
def f(x: int | None) -> str: ...
Prod exception discipline
Catch specific exceptions. Never bare
except: — silences KeyboardInterrupt and SystemExit.# WRONG
try: ...
except: ... # catches everything!
# RIGHT
try: ...
except (ValueError, KeyError) as e:
logger.error("msg", exc_info=e)
raise # re-raise if needed
Prod __all__ & module design
Define __all__ to control public API. Prevents * imports from leaking internals.
__all__ = ['PublicClass', 'public_fn']
# prefix with _ for internal
def _internal_helper(): ...
# __init__.py re-exports
from .module import PublicClass
Topics juniors consistently get wrong
Mentor EAFP vs LBYL
Python prefers EAFP (Easier to Ask Forgiveness) over LBYL (Look Before You Leap). It's faster and more Pythonic.
# LBYL (non-Pythonic)
if key in d and d[key] is not None: ...
# EAFP (Pythonic)
try:
val = d[key]
except KeyError:
val = default
Mentor list vs generator — when each
Use a list if you need: length, indexing, multiple iteration, slicing. Use a generator if you need: one-pass, memory efficiency, streaming, pipeline chaining.
# pipeline — no intermediate lists
result = sum(
x*x
for x in filter(lambda n: n%2, data)
if x > 10
)
Mentor duck typing & protocols
Don't isinstance-check — check behaviour. Protocol (3.8+) gives structural subtyping without inheritance.
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
# any class with .draw() satisfies
# Drawable — no inheritance needed
Mentor unpacking operators
* and ** in function calls, assignments, and collections. Juniors underuse these.
a, *rest, last = [1, 2, 3, 4, 5]
merged = {**dict1, **dict2} # merge dicts
combined = [*list1, *list2] # merge lists
def f(*args, **kwargs): ...
f(*my_list, **my_dict)
Mentor comprehension readability threshold
Nested list comprehensions with conditions are hard to review. If it doesn't fit on one clean line, use a loop.
# hard to review
[[f(x) for x in row if p(x)] for row in mat]
# readable
result = []
for row in mat:
result.append([f(x) for x in row if p(x)])
Mentor pytest best practices
One assert per test. Test behaviour, not implementation. Fixtures over setUp/tearDown. Parametrize for edge cases.
import pytest
@pytest.mark.parametrize("n,expected", [
(0, 0), (1, 1), (10, 55)
])
def test_fib(n, expected):
assert fib(n) == expected
Code review checklist to share with juniors
no bare except
no mutable defaults
type hints on public API
generators for large data
pathlib not os.path
f-strings not %
== not is for values
context managers for resources
deepcopy when needed
log don't print
docstring on public fns
no wildcard imports