Python
Python From Zero to Interview-Ready
A complete guide to learning Python and cracking the coding interview
How to use this guide
This is long on purpose. It is meant to be a reference you return to, not something you read once.
There are four parts:
| Part | What it covers | Who it's for |
|---|---|---|
| 1. Introduction to Python | What Python is, how it works, what it's good and bad at, how to set it up | Absolute beginners |
| 2. Learning Python | A 12-week phased roadmap from syntax to writing real code | Beginners → intermediate |
| 3. The interview roadmap | Data structures, algorithms, patterns, and a week-by-week study plan | Anyone job hunting |
| 4. Interview execution | How to actually behave in a 45-minute interview | Anyone with interviews booked |
If you already know Python, skip to Part 3. If you know Python and DSA, skip to Part 4 — that's where most people with strong fundamentals still lose offers.
One honest expectation-setting note before we start: going from "never programmed" to "passing FAANG-level interviews" takes most people 8–14 months of consistent effort, not six weeks. If you already program in another language, 3–5 months is realistic. Anyone promising faster is selling something.
Part 1 — Introduction to Python
1.1 What Python actually is
Python is a high-level, interpreted, dynamically typed, garbage-collected, general-purpose programming language. That's a mouthful, so let's take it apart:
- High-level — you don't manage memory, pointers, or CPU registers. You write
names.append("Ada")instead of allocating heap blocks. - Interpreted — you run source files directly (
python app.py) without a separate compile step. (Technically Python is compiled, to bytecode — more on that below.) - Dynamically typed — a variable's type is checked at runtime, not declared up front.
x = 5thenx = "five"is legal. - Garbage-collected — memory is reclaimed automatically when objects are no longer referenced.
- General-purpose — not a niche tool. It runs web backends, machine learning models, data pipelines, automation scripts, embedded devices, and scientific research.
It was created by Guido van Rossum and released in 1991. The name comes from Monty Python's Flying Circus, not the snake — which is why so much Python documentation historically used "spam" and "eggs" as example variables.
The current major version is Python 3. Python 2 is dead (end of life: January 2020). If a tutorial has print "hello" without parentheses, close the tab.
1.2 Why Python is worth learning
It reads like pseudocode. Compare summing a list of numbers.
Java:
int total = 0;
for (int i = 0; i < numbers.length; i++) {
total += numbers[i];
}
Python:
total = sum(numbers)
That gap compounds. Less code means fewer bugs, faster iteration, and — importantly for you — less mental overhead during a timed interview when you're already stressed. This is the single biggest reason Python dominates as an interview language.
The ecosystem is unmatched in breadth. There are over 500,000 packages on PyPI, the Python Package Index. Whatever you need — HTTP requests, PDF parsing, neural networks, web scraping, Excel manipulation, cryptography — someone has already built a good library.
It is the default language of AI and data. PyTorch, TensorFlow, JAX, scikit-learn, pandas, NumPy, and effectively every major ML framework has Python as its primary interface. If you want to work anywhere near machine learning, Python is not optional.
Career demand is broad and durable. Python consistently sits in the top three of the TIOBE index, Stack Overflow's developer survey, and GitHub's language rankings. It is used at Google, Netflix, Instagram (one of the largest Django deployments in the world), Spotify, Dropbox, NASA, and JPMorgan. This matters for job security: you're not betting your career on a trend.
The learning curve is gentle but the ceiling is high. You can write something useful in week one. You can also write a distributed system, a compiler, or a game engine in it. Few languages give you both.
1.3 Where Python is honestly the wrong choice
Being clear-eyed about this makes you a better engineer, and interviewers notice when you can discuss tradeoffs.
- Raw CPU speed. Python is roughly 10–100× slower than C for tight numeric loops. The workaround is that the heavy lifting in libraries like NumPy is written in C or Fortran, so you're often orchestrating fast code rather than running slow code. But a pure-Python physics engine is a bad idea.
- True CPU parallelism in one process. Historically, CPython's Global Interpreter Lock (GIL) allowed only one thread to execute Python bytecode at a time. Threads still help with I/O-bound work (waiting on network or disk), but for CPU-bound work you reach for
multiprocessingor a native extension. Python 3.13 introduced an experimental free-threaded build that removes the GIL, and 3.14 continued that work, but as of now most production deployments still run with the GIL. - Mobile apps. No serious native iOS/Android story. Use Swift, Kotlin, or a cross-platform framework.
- Front-end browser code. JavaScript owns that. (Tools like Pyodide compile Python to WebAssembly, but it's niche.)
- Very small memory footprints. A Python object carries significant overhead compared to a C struct. Microcontrollers use MicroPython or C.
- Large codebases without discipline. Dynamic typing that feels liberating in a 200-line script becomes dangerous in 200,000 lines. Mature teams solve this with type hints and
mypy, but it's opt-in, not enforced.
1.4 How Python runs your code
Understanding this makes debugging and performance conversations much easier, and it's a common "do you actually know your language" interview question.
your_file.py
│
▼ 1. Lexing & parsing → Abstract Syntax Tree
│
▼ 2. Compilation → bytecode (cached in __pycache__/*.pyc)
│
▼ 3. Execution → CPython Virtual Machine evaluates bytecode
│
▼ Output
So Python is compiled — just to bytecode for a virtual machine, not to native machine code. Those __pycache__ folders you see are compilation caches. Deleting them is harmless; they regenerate.
You can inspect bytecode yourself:
import dis
def add(a, b):
return a + b
dis.dis(add)
"CPython" is the reference implementation — the one you download from python.org, written in C. Alternatives exist and are worth knowing by name:
| Implementation | What's different | When you'd use it |
|---|---|---|
| CPython | The standard | Almost always |
| PyPy | JIT compiler, often 4–10× faster | Long-running CPU-bound pure-Python code |
| MicroPython | Tiny footprint | Microcontrollers, embedded |
| Jython / IronPython | Runs on JVM / .NET | Legacy interop (both lag badly on version support) |
1.5 Everything is an object
This is the mental model that unlocks Python. Integers, strings, functions, classes, modules, even types themselves are objects with identity, a type, and attributes.
x = 42
print(type(x)) # <class 'int'>
print(x.bit_length()) # 6 — integers have methods
print((42).__add__(8)) # 50 — the + operator calls a method
def greet():
return "hi"
print(type(greet)) # <class 'function'>
greet.author = "me" # functions can hold attributes
print(greet.author) # me
functions = [greet, len, str.upper] # functions live in data structures
A consequence worth internalizing early: variables are names bound to objects, not boxes holding values. Assignment binds a name; it doesn't copy.
a = [1, 2, 3]
b = a # b and a name the SAME list
b.append(4)
print(a) # [1, 2, 3, 4] ← surprised? this trips up everyone once
c = a.copy() # now c is a separate list (shallow copy)
c.append(5)
print(a) # [1, 2, 3, 4] — unchanged
This single concept explains a huge share of beginner bugs and shows up in interviews as questions about mutability and aliasing.
1.6 Setting up your environment
Install Python. Get it from python.org or use a version manager. On macOS, do not use the system Python that ships with the OS — install your own via Homebrew (brew install python) or pyenv. On Windows, check "Add Python to PATH" during install. On Linux it's likely already there; verify with:
python3 --version # should print 3.11 or newer
Learn virtual environments on day one. Do not skip this. A virtual environment is an isolated folder of packages for one project, so Project A's requirement of pandas 1.5 doesn't break Project B's need for pandas 2.2.
python3 -m venv .venv # create
source .venv/bin/activate # activate (macOS/Linux)
.venv\Scripts\activate # activate (Windows)
pip install requests # installs into .venv only
pip freeze > requirements.txt # record dependencies
pip install -r requirements.txt # reproduce elsewhere
deactivate # exit
Add .venv/ to your .gitignore. Never commit a virtual environment.
Modern alternatives worth knowing: uv (extremely fast, written in Rust, increasingly the default in new projects), Poetry (dependency resolution and packaging), and conda (heavier, popular in data science because it manages non-Python binaries too).
Pick an editor. VS Code with the Python and Pylance extensions is the mainstream free choice. PyCharm is more powerful out of the box, especially for debugging and refactoring. Jupyter notebooks are excellent for data exploration and terrible for building software — use them to explore, not to structure a codebase.
Configure these three tools early. They make you look and feel like a professional:
pip install ruff mypy pytest
ruff check . # linting + formatting, very fast
mypy app.py # static type checking
pytest # run tests
1.7 Your first tour of the language
Rather than a "hello world" you'll forget, here's a compressed tour of the syntax you'll use 90% of the time. Type it out — don't copy-paste. Motor memory matters.
# ---------- Variables and types ----------
name = "Ada" # str
age = 36 # int
height = 1.68 # float
is_engineer = True # bool
nothing = None # NoneType
# f-strings: the modern way to format
print(f"{name} is {age} years old, {height:.1f}m tall")
print(f"{age = }") # debugging shortcut → age = 36
# ---------- Control flow ----------
if age >= 18:
status = "adult"
elif age >= 13:
status = "teen"
else:
status = "child"
# Indentation IS the syntax. 4 spaces. No braces. Be consistent.
for i in range(3): # 0, 1, 2
print(i)
for index, char in enumerate("abc"):
print(index, char)
n = 5
while n > 0:
n -= 1
if n == 2:
continue # skip rest of this iteration
if n == 1:
break # exit loop entirely
# ---------- The four core collections ----------
nums = [3, 1, 2] # list — ordered, mutable
point = (4, 5) # tuple — ordered, immutable
unique = {1, 2, 3} # set — unordered, no duplicates
ages = {"Ada": 36, "Alan": 41} # dict — key → value
nums.append(4)
nums.sort() # in place → [1, 2, 3, 4]
print(sorted(nums, reverse=True)) # returns new list → [4, 3, 2, 1]
x, y = point # unpacking
first, *rest = [1, 2, 3, 4] # first=1, rest=[2, 3, 4]
for key, value in ages.items():
print(key, value)
# ---------- Comprehensions: the Python signature move ----------
squares = [n * n for n in range(10)]
evens = [n for n in range(20) if n % 2 == 0]
lookup = {word: len(word) for word in ["hi", "there"]}
distinct = {c for c in "mississippi"}
lazy = (n * n for n in range(10**9)) # generator: computed on demand
# ---------- Functions ----------
def area(width, height=1, *args, **kwargs):
"""Docstrings go here. Triple quotes."""
return width * height
def stats(values: list[int]) -> tuple[int, float]: # type hints: optional, valuable
return sum(values), sum(values) / len(values)
total, mean = stats([1, 2, 3])
double = lambda n: n * 2 # anonymous function; use sparingly
# ---------- Errors ----------
try:
result = 10 / 0
except ZeroDivisionError as exc:
print(f"caught: {exc}")
except (TypeError, ValueError):
print("bad input")
else:
print("no exception occurred")
finally:
print("always runs — cleanup goes here")
# ---------- Classes ----------
class Account:
interest_rate = 0.02 # class attribute, shared
def __init__(self, owner, balance=0): # constructor
self.owner = owner # instance attributes
self._balance = balance # leading _ means "internal, please don't touch"
def deposit(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
self._balance += amount
return self._balance
@property
def balance(self): # accessed as obj.balance, not obj.balance()
return self._balance
def __repr__(self): # what you see in the debugger
return f"Account({self.owner!r}, {self._balance})"
acct = Account("Ada", 100)
acct.deposit(50)
print(acct.balance, acct) # 150 Account('Ada', 150)
# ---------- Context managers: guaranteed cleanup ----------
with open("data.txt", "w") as f:
f.write("hello")
# file is closed automatically, even if an exception was raised
If you understand every line above, you know enough syntax to start solving real problems. Everything else is libraries and practice.
1.8 The Zen of Python
Run import this in a Python shell. You get a short poem of design principles by Tim Peters. The lines that matter most in practice:
- Readability counts. Code is read far more often than it's written.
- Explicit is better than implicit. Don't be clever at the reader's expense.
- Simple is better than complex. Reach for the boring solution first.
- There should be one — and preferably only one — obvious way to do it. This is why the community converges on shared idioms so strongly.
- Errors should never pass silently. A bare
except: passis almost always a bug you haven't found yet.
"Pythonic" code means code that follows these idioms. It's not pedantry — an interviewer watching you write for i in range(len(arr)): print(arr[i]) instead of for item in arr: print(item) will quietly downgrade their read of your fluency.
Part 2 — How to Actually Learn Python
2.1 The learning principle that matters more than any resource
You cannot learn programming by watching. You learn it by producing code that doesn't work and then making it work.
Watching a tutorial creates the feeling of understanding — the instructor's code runs, it makes sense, you nod along. That feeling is fluency in reading, not writing. Then you open a blank file and freeze. This is the single most common failure mode in self-taught programming, and it has a name: tutorial hell.
The fix is uncomfortable and simple: for every 20 minutes of consuming, spend 40 minutes producing. Specifically:
- Watch/read a concept once. No re-watching.
- Close the tutorial. Rebuild the example from memory. You will fail. Good — the struggle is where learning happens.
- Break it deliberately. Change something and predict the outcome before running it. Wrong predictions are the most valuable data you'll get about your own mental model.
- Extend it beyond what was taught. The tutorial made a to-do list? Add due dates, sorting, and persistence to a file.
- Explain it out loud as if teaching someone. Gaps become audible immediately.
Two techniques with strong evidence behind them are worth building in explicitly. Spaced repetition: revisit a concept after 1 day, 3 days, 1 week, 3 weeks. Retrieval practice: instead of re-reading notes, close them and write down everything you remember. Both feel less productive than re-reading, and both work substantially better.
2.2 The 12-week Python roadmap
This assumes ~10–12 hours per week. Adjust the calendar, not the sequence. Do not move to the next phase until you can build the checkpoint project without looking anything up except official docs.
Phase 0 — Setup (2–3 hours, do it once)
Install Python, create a virtual environment, install VS Code, write and run one file, initialize a Git repository, push it to GitHub.
That last part matters. Start committing from day one, even for throwaway practice files. Twelve weeks from now you'll have a visible history of consistent work, and that history is a genuinely useful signal to recruiters.
Checkpoint: you can create a fresh project with a venv, install a package, commit, and push without a guide.
Phase 1 — Core syntax (Weeks 1–3)
| Week | Topics |
|---|---|
| 1 | Variables, types, operators, input()/print(), f-strings, if/elif/else, while, for, range, break/continue |
| 2 | Lists and list methods, tuples, strings and string methods, slicing, indexing, nested loops |
| 3 | Dictionaries, sets, functions, parameters vs arguments, default args, *args/**kwargs, return values, scope, import |
Concepts people consistently get wrong here — slow down on these:
- Slicing.
arr[start:stop:step],stopis exclusive, negatives count from the end.arr[::-1]reverses.arr[:]copies. Master it now; you'll use it in every interview. - Mutable vs immutable. Lists, dicts, and sets can be changed in place. Strings, tuples, ints, and frozensets cannot. This determines what can be a dict key (only hashable/immutable things).
- In-place vs returning.
list.sort()returnsNoneand mutates;sorted(list)returns a new list.list.append()mutates. Confusing these producesAttributeError: 'NoneType' object has no attribute...— the single most common beginner traceback. - The mutable default argument trap.
def bad(item, basket=[]): # the SAME list is reused across every call
basket.append(item)
return basket
print(bad("a")) # ['a']
print(bad("b")) # ['a', 'b'] ← almost never what you wanted
def good(item, basket=None): # the correct pattern
if basket is None:
basket = []
basket.append(item)
return basket
Checkpoint projects — build all three:
- Number guessing game with input validation and a retry loop.
- Command-line to-do list with add/remove/list/mark-complete, held in memory.
- Text statistics tool that takes a paragraph and reports word count, unique words, the five most common words, and average word length.
Phase 2 — Idiomatic Python and data structures (Weeks 4–6)
| Week | Topics |
|---|---|
| 4 | Comprehensions (list/dict/set), generators and yield, enumerate, zip, any/all, min/max with key= |
| 5 | Sorting deeply (key=, stability, multi-key, reverse), lambdas, map/filter vs comprehensions, unpacking, */** splatting |
| 6 | Nested data structures, JSON, reading/writing files, pathlib, CSV, exceptions in depth, custom exception classes |
This is the phase where you stop writing Java-with-Python-syntax and start writing Python. Concretely:
# Not Pythonic # Pythonic
i = 0 for item in items:
while i < len(items): print(item)
print(items[i])
i += 1
result = [] result = [x * 2 for x in nums if x > 0]
for x in nums:
if x > 0:
result.append(x * 2)
if len(items) > 0: if items:
if x == None: if x is None:
for i in range(len(names)): for i, name in enumerate(names):
print(i, names[i])
for i in range(len(a)): for x, y in zip(a, b):
print(a[i], b[i])
temp = a; a = b; b = temp a, b = b, a
Generators deserve special attention because they're both idiomatic and genuinely useful for memory efficiency:
def read_large_file(path):
"""Yields one line at a time — never loads the whole file into memory."""
with open(path) as f:
for line in f:
yield line.strip()
# Nothing is read until you iterate
for line in read_large_file("10gb.log"):
if "ERROR" in line:
print(line)
Checkpoint projects:
- Expense tracker that persists to JSON, supports categories, and reports monthly totals sorted by amount.
- Log file analyzer that streams a large file with a generator and reports error counts by type and hour.
- CSV → report tool that reads a messy CSV, cleans it, and writes a summary CSV. (Handle missing values and bad rows without crashing.)
Phase 3 — Object-oriented Python and structure (Weeks 7–9)
| Week | Topics |
|---|---|
| 7 | Classes, __init__, instance vs class attributes, methods, @property, @staticmethod, @classmethod, __repr__/__str__ |
| 8 | Inheritance, composition, super(), method resolution order, dunder methods (__len__, __eq__, __iter__, __hash__), dataclasses, Enum |
| 9 | Modules and packages, if __name__ == "__main__":, project layout, decorators, functools, closures, type hints and mypy |
A framing that helps: classes are for bundling state with the behavior that operates on it. If your class has no state — just methods that take everything as arguments — you wanted functions in a module.
dataclasses remove most OOP boilerplate and are what modern Python actually uses:
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
priority: int = 3
tags: list[str] = field(default_factory=list) # note: NOT tags: list = []
done: bool = False
def complete(self) -> None:
self.done = True
t = Task("Write article", priority=1, tags=["writing"])
print(t) # Task(title='Write article', priority=1, tags=['writing'], done=False)
# __init__, __repr__, and __eq__ are generated for you
Decorators look like magic until you see that they're just functions that take a function and return a function:
import functools, time
def timed(func):
@functools.wraps(func) # preserves the original name and docstring
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.perf_counter() - start:.4f}s")
return result
return wrapper
@timed # equivalent to: slow = timed(slow)
def slow():
time.sleep(0.5)
slow() # slow took 0.5004s
Checkpoint project: a library management system — Book, Member, and Library classes; borrowing with due dates and limits; custom exceptions (BookUnavailableError); JSON persistence; a proper package layout with __init__.py; and type hints throughout that pass mypy.
Phase 4 — Professional practice (Weeks 10–12)
| Week | Topics |
|---|---|
| 10 | Testing with pytest, fixtures, parametrized tests, mocking, test-driven development, coverage |
| 11 | Standard library depth: collections, itertools, datetime, re, os/sys, argparse, logging, json, random |
| 12 | Git workflow (branches, merges, pull requests), reading other people's code, debugging with pdb/breakpoints, virtual environments and dependency management, ruff + mypy in CI |
Testing is the skill that most separates hobbyists from employable engineers, and it's the one self-taught developers skip most often. Learn it here:
# test_calculator.py
import pytest
from calculator import divide
def test_divides_normally():
assert divide(10, 2) == 5
@pytest.mark.parametrize("a,b,expected", [(6, 3, 2), (-6, 3, -2), (0, 5, 0)])
def test_various_inputs(a, b, expected):
assert divide(a, b) == expected
def test_raises_on_zero():
with pytest.raises(ZeroDivisionError):
divide(1, 0)
Also learn logging and delete your print() debugging habit:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
log = logging.getLogger(__name__)
log.info("Processing %d records", len(records)) # lazy formatting — no f-string
log.warning("Retrying after failure")
log.exception("Unrecoverable") # includes the full traceback
Checkpoint project: take your Phase 3 library system and add a full pytest suite above 80% coverage, an argparse CLI, structured logging, a pyproject.toml, a README with usage examples, and a GitHub Actions workflow that runs ruff, mypy, and pytest on every push. This project alone is a credible portfolio piece.
Phase 5 — Pick a specialization (Month 4+)
You now know Python. Depth comes from applying it to a domain. Pick one — spreading across three gets you hired for none.
| Track | Core stack | Build this |
|---|---|---|
| Backend / web | FastAPI or Django, PostgreSQL, SQLAlchemy, Redis, Docker, REST/auth | A deployed API with authentication, a database, tests, and Docker |
| Data / analytics | pandas, NumPy, Polars, SQL, matplotlib/Plotly, Jupyter, dbt | An end-to-end analysis of a real messy dataset with a written conclusion |
| ML / AI | scikit-learn, PyTorch, Hugging Face, MLflow | A trained model served behind an API, with honest evaluation metrics |
| Automation / DevOps | requests, Playwright, Celery, Airflow, boto3, Bash, CI/CD |
A scheduled pipeline that scrapes, transforms, and reports on real data |
| Data engineering | Spark/PySpark, Kafka, Airflow, warehouses | A batch + streaming pipeline with orchestration and data quality checks |
2.3 Where to learn from
Curated over comprehensive — three great resources finished beat fifteen bookmarked.
Free and genuinely excellent:
- The official Python Tutorial at docs.python.org. Underrated. Read it once you know basics; it's clearer than most paid courses.
- Automate the Boring Stuff with Python (Al Sweigart) — free online, best possible starting point for absolute beginners because every chapter produces something useful.
- Real Python — deep, accurate articles on specific topics. Excellent for "how does X actually work."
- CS50P (Harvard's Introduction to Programming with Python) — free, rigorous, well-taught, with real problem sets.
- Fluent Python (Luciano Ramalho) — not free, but the definitive intermediate→advanced book. Read it in month 4+, not month 1.
Practice platforms:
- Exercism — Python track with human mentorship on your submissions. The feedback on idiom is uniquely valuable.
- Codewars / HackerRank — quick syntax reps.
- Advent of Code — annual puzzle set; excellent for building problem-solving stamina.
- LeetCode / NeetCode — for interviews specifically. See Part 3.
A note on AI assistants: they are extraordinary learning tools and a genuine trap. Used well — "explain why this fails," "what's the Pythonic version of this," "quiz me on generators" — they compress months of learning. Used badly — "write my project" — they produce a portfolio you cannot defend in an interview and skills you don't have. My suggested rule: write it yourself first, then ask for a critique. And never let an AI write code you couldn't explain line by line to an interviewer, because you will be asked to.
2.4 The six mistakes that stall most learners
- Collecting resources instead of finishing one. Pick a course. Finish it. Then evaluate.
- Skipping the hard chapters — usually OOP, decorators, and testing. These are exactly the chapters that separate you from other candidates.
- Never building anything unprompted. Projects that come from your own annoyance ("I hate manually renaming these files") teach far more than assigned ones, and they're what you actually talk about in interviews.
- Debugging by random mutation. Learn to read tracebacks bottom-up: the last line is the error type and message, and the line above it is where it happened in your code. Then use
breakpoint()and inspect actual values instead of guessing. - Studying alone in silence. Join a Discord, answer questions on Stack Overflow, do code review with someone. Explaining exposes gaps nothing else finds.
- Waiting to feel ready. Nobody feels ready. Start applying and doing mock interviews while you're still studying — the feedback redirects your effort better than any curriculum.
Part 3 — The Coding Interview Roadmap
3.1 What coding interviews actually test
A common misconception is that interviews test whether you've memorized enough problems. They don't. A typical 45-minute technical round scores you on roughly four axes:
- Problem solving — can you decompose an unfamiliar problem, recognize its underlying structure, and reason about tradeoffs?
- Coding — can you translate a plan into correct, clean code at a reasonable pace?
- Verification — do you test your own work, or do you announce "done" and wait to be told it's broken?
- Communication — can the interviewer follow your thinking? Can they collaborate with you when you're stuck?
Grinding problems only improves axis 1, and only indirectly. Candidates who have solved 600 problems and still fail are almost always failing on axes 3 and 4. Plan your preparation accordingly: solving fewer problems while thinking out loud and testing rigorously beats solving more in silence.
The other reframe that helps: the goal is pattern recognition, not problem recall. There are roughly 15–20 recurring patterns. Once you can look at a new problem and think "sorted array, looking for a pair — two pointers," you can solve problems you've never seen. That's the actual skill.
3.2 Your Python interview toolkit
Python's advantage in interviews is that the standard library hands you most data structures for free. Know these cold.
Complexity of built-in operations
This table is worth memorizing. Interviewers ask about it directly, and misjudging it silently makes your solution quadratic.
| Operation | Structure | Average | Note |
|---|---|---|---|
arr[i] |
list | O(1) | |
arr.append(x) |
list | O(1) amortized | |
arr.pop() |
list | O(1) | from the end |
arr.pop(0) / arr.insert(0, x) |
list | O(n) | shifts everything — use deque |
x in arr |
list | O(n) | the most common accidental O(n²) |
x in s |
set / dict | O(1) | |
s.add(x) / d[k] = v |
set / dict | O(1) | |
arr.sort() / sorted() |
list | O(n log n) | Timsort, stable |
min / max / sum |
list | O(n) | |
heapq.heappush/heappop |
heap | O(log n) | |
heap[0] |
heap | O(1) | peek the minimum |
heapq.heapify(arr) |
list → heap | O(n) | not O(n log n) |
dq.appendleft/popleft |
deque | O(1) | |
bisect_left/right |
sorted list | O(log n) | but inserting is O(n) |
s1 & s2, s1 | s2 |
sets | O(min/sum of sizes) | |
"a" + "b" in a loop |
str | O(n²) total | strings are immutable — use "".join(parts) |
arr[a:b] |
list / str | O(b − a) | slicing copies |
The four imports you'll use constantly
from collections import defaultdict, Counter, deque, OrderedDict
import heapq
from functools import lru_cache, cmp_to_key, reduce
import bisect
# ---------- defaultdict: no more KeyError ----------
from collections import defaultdict
graph = defaultdict(list)
graph["a"].append("b") # key created automatically
counts = defaultdict(int)
for c in "hello":
counts[c] += 1 # no need to initialize
# ---------- Counter: frequency in one line ----------
from collections import Counter
c = Counter("mississippi")
print(c) # Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
print(c.most_common(2)) # [('i', 4), ('s', 4)]
print(Counter("abc") == Counter("cba")) # True → instant anagram check
# ---------- deque: O(1) at both ends ----------
from collections import deque
dq = deque([1, 2, 3])
dq.appendleft(0) # O(1) — list.insert(0, x) is O(n)
dq.popleft() # O(1)
dq = deque(maxlen=3) # auto-evicts from the other end
# ---------- heapq: min-heap only ----------
import heapq
h = []
heapq.heappush(h, 5)
heapq.heappush(h, 1)
print(h[0]) # 1 — smallest, O(1) peek
heapq.heappop(h) # removes and returns 1
# max-heap trick: negate the values
maxh = []
heapq.heappush(maxh, -5)
largest = -heapq.heappop(maxh) # 5
# top-k without sorting everything: O(n log k)
print(heapq.nlargest(3, [5, 1, 8, 3, 9])) # [9, 8, 5]
# heap of tuples sorts by first element, then second
heapq.heappush(h, (priority, task_id, task))
# ---------- bisect: binary search on sorted lists ----------
import bisect
arr = [1, 3, 3, 5, 7]
bisect.bisect_left(arr, 3) # 1 → first index where 3 could go
bisect.bisect_right(arr, 3) # 3 → last index where 3 could go
bisect.insort(arr, 4) # inserts keeping order (O(n) for the shift)
# ---------- lru_cache: memoization for free ----------
from functools import lru_cache
@lru_cache(maxsize=None) # or @cache in Python 3.9+
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(200)) # instant; without the cache, effectively never finishes
Python interview gotchas that cost people offers
# 1. Integer division and negative numbers
print(7 // 2) # 3
print(-7 // 2) # -4 ← floors toward negative infinity, NOT -3
print(int(-7 / 2)) # -3 ← use this when you want truncation
print(-7 % 3) # 2 ← Python's modulo returns the sign of the divisor
# 2. Recursion limit — Python's default is ~1000 frames
import sys
sys.setrecursionlimit(10**6) # mention this if a DFS could go 10^5 deep
# Better: convert deep recursion to an iterative stack-based solution
# 3. Copying 2D grids
grid = [[0] * 3] * 3 # BROKEN — three references to ONE row
grid[0][0] = 1
print(grid) # [[1,0,0],[1,0,0],[1,0,0]] ← all rows changed
grid = [[0] * 3 for _ in range(3)] # correct
# 4. Mutating a list while iterating it
for x in nums:
if x < 0:
nums.remove(x) # skips elements; silently wrong
nums = [x for x in nums if x >= 0] # correct
# 5. Sorting by multiple keys, mixed directions
people.sort(key=lambda p: (-p.score, p.name)) # score desc, then name asc
# 6. Infinity as a sentinel
best = float("inf")
worst = float("-inf")
# 7. Custom comparators when a key function isn't enough
from functools import cmp_to_key
words.sort(key=cmp_to_key(lambda a, b: 1 if a + b < b + a else -1))
# 8. Unlimited integer precision — no overflow in Python
print(2 ** 200) # works fine; mention this when an interviewer asks about overflow
3.3 Complexity analysis: the part you must be fluent in
You will be asked "what's the time and space complexity?" in essentially every interview. Answer without hesitation.
Growth rates, best to worst:
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(n³) < O(2ⁿ) < O(n!)
Reverse-engineer the target complexity from the constraints. This is a genuinely powerful trick that most candidates don't use:
| Constraint on n | Likely intended complexity |
|---|---|
| n ≤ 10 | O(n!) or O(2ⁿ) — permutations, brute-force search |
| n ≤ 20 | O(2ⁿ) — subsets, bitmask DP |
| n ≤ 500 | O(n³) — Floyd–Warshall, interval DP |
| n ≤ 5,000 | O(n²) — nested loops, 2D DP |
| n ≤ 10⁵ | O(n log n) — sorting, heaps, binary search |
| n ≤ 10⁶ | O(n) or O(n log n) — single pass, hashing |
| n ≥ 10⁹ | O(log n) or O(1) — binary search on the answer, math |
If the problem says n ≤ 10⁵ and you're describing an O(n²) approach, the interviewer already knows you're not done. Say so yourself: "this is O(n²), which won't pass at n = 10⁵, so let me improve it."
Space complexity counts the recursion stack. A DFS on a tree of height h uses O(h) space even with no explicit data structures. Interviewers listen for whether you mention this.
3.4 Topic-by-topic roadmap
For each topic: what to learn, the patterns that appear, and representative problems. Names are given rather than links, so search them on LeetCode or NeetCode.
1. Arrays and strings — the foundation
Learn: traversal, two-pointer, in-place modification, prefix sums, sorted() with keys, string immutability.
Patterns:
Two pointers (opposite ends) — sorted arrays, palindromes, pair-finding.
def two_sum_sorted(nums, target):
left, right = 0, len(nums) - 1
while left < right:
total = nums[left] + nums[right]
if total == target:
return [left, right]
elif total < target:
left += 1 # need bigger
else:
right -= 1 # need smaller
return []
Fast/slow pointers (same direction) — in-place removal, deduplication, cycle detection.
def remove_duplicates(nums):
"""Sorted array; returns new length, modifies in place."""
if not nums:
return 0
write = 1
for read in range(1, len(nums)):
if nums[read] != nums[write - 1]:
nums[write] = nums[read]
write += 1
return write
Prefix sums — repeated range-sum queries, subarray sums.
def subarray_sum_equals_k(nums, k):
"""Count subarrays summing to k. O(n)."""
from collections import defaultdict
seen = defaultdict(int)
seen[0] = 1 # empty prefix
running = count = 0
for n in nums:
running += n
count += seen[running - k] # how many prefixes make up the difference
seen[running] += 1
return count
Problems: Two Sum · Best Time to Buy and Sell Stock · Contains Duplicate · Product of Array Except Self · Maximum Subarray (Kadane's) · Valid Palindrome · Valid Anagram · Group Anagrams · 3Sum · Container With Most Water · Merge Intervals · Rotate Array · Longest Common Prefix · Encode and Decode Strings
2. Hashing — the highest-leverage technique
Learn: dict and set as O(1) lookup, defaultdict, Counter, choosing a good key, frequency maps, seen-sets.
The core insight: almost any O(n²) "check every pair" solution becomes O(n) by trading space for time with a hash map. If you find yourself writing nested loops, ask "what could I store to avoid the inner loop?"
def two_sum(nums, target):
"""Unsorted. O(n) time, O(n) space."""
seen = {} # value → index
for i, n in enumerate(nums):
if target - n in seen: # have we already passed the complement?
return [seen[target - n], i]
seen[n] = i
return []
def group_anagrams(words):
"""Key insight: choose a key that's identical for all anagrams."""
from collections import defaultdict
groups = defaultdict(list)
for w in words:
key = tuple(sorted(w)) # or a 26-length count tuple for O(n·k)
groups[key].append(w)
return list(groups.values())
Problems: Two Sum · Valid Anagram · Group Anagrams · Top K Frequent Elements · Longest Consecutive Sequence · Longest Substring Without Repeating Characters · Subarray Sum Equals K · First Missing Positive · LRU Cache
3. Sliding window
Learn: fixed-size vs variable-size windows, the expand/contract loop, when a window is valid.
Recognize it when: the problem asks about a contiguous subarray or substring that is longest/shortest/optimal subject to a condition.
Variable-size template — memorize this shape:
def longest_substring_without_repeats(s):
window = set()
left = best = 0
for right, ch in enumerate(s):
while ch in window: # contract until valid again
window.remove(s[left])
left += 1
window.add(ch) # expand
best = max(best, right - left + 1)
return best
Fixed-size template:
def max_sum_subarray_k(nums, k):
window = sum(nums[:k])
best = window
for i in range(k, len(nums)):
window += nums[i] - nums[i - k] # slide: add new, drop old
best = max(best, window)
return best
Problems: Best Time to Buy and Sell Stock · Longest Substring Without Repeating Characters · Longest Repeating Character Replacement · Permutation in String · Minimum Window Substring · Sliding Window Maximum · Maximum Average Subarray
4. Stacks and queues
Learn: stack as a list, queue as a deque, monotonic stacks, matching-pair problems, expression parsing.
Monotonic stack — the pattern for "next greater/smaller element," histograms, and temperature-style problems. It looks intimidating and is actually short:
def daily_temperatures(temps):
"""For each day, days until a warmer one. O(n) — each index pushed/popped once."""
answer = [0] * len(temps)
stack = [] # indices, temps decreasing
for i, t in enumerate(temps):
while stack and temps[stack[-1]] < t: # found a warmer day for these
j = stack.pop()
answer[j] = i - j
stack.append(i)
return answer
def is_valid_parentheses(s):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for ch in s:
if ch in pairs:
if not stack or stack.pop() != pairs[ch]:
return False
else:
stack.append(ch)
return not stack
Problems: Valid Parentheses · Min Stack · Evaluate Reverse Polish Notation · Daily Temperatures · Car Fleet · Largest Rectangle in Histogram · Generate Parentheses · Implement Queue using Stacks · Basic Calculator
5. Linked lists
Learn: node structure, dummy heads, in-place reversal, fast/slow pointers, merging.
Interviewers love linked lists precisely because they can't be brute-forced — you need pointer discipline. Two tricks solve most of them: a dummy head node, and fast/slow pointers.
class ListNode:
def __init__(self, val=0, next=None):
self.val, self.next = val, next
def reverse_list(head):
prev, curr = None, head
while curr:
curr.next, prev, curr = prev, curr, curr.next # the classic three-way swap
return prev
def has_cycle(head):
"""Floyd's tortoise and hare. O(1) space."""
slow = fast = head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
if slow is fast:
return True
return False
def merge_two_sorted(a, b):
dummy = tail = ListNode() # dummy head avoids special-casing the first node
while a and b:
if a.val <= b.val:
tail.next, a = a, a.next
else:
tail.next, b = b, b.next
tail = tail.next
tail.next = a or b # attach whatever remains
return dummy.next
Problems: Reverse Linked List · Merge Two Sorted Lists · Linked List Cycle · Reorder List · Remove Nth Node From End · Copy List with Random Pointer · Add Two Numbers · LRU Cache · Merge K Sorted Lists · Reverse Nodes in K-Group
6. Binary search
Learn: the classic form, bisect, and — most importantly — binary search on the answer.
Most candidates know basic binary search and stop there. The higher-value skill is recognizing that you can binary search over any monotonic answer space, even when there's no sorted array in sight.
def binary_search(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = (lo + hi) // 2 # Python ints don't overflow
if nums[mid] == target:
return mid
elif nums[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
def min_eating_speed(piles, hours):
"""Binary search on the ANSWER: what's the smallest speed that finishes in time?"""
def hours_needed(speed):
return sum(-(-p // speed) for p in piles) # ceiling division
lo, hi = 1, max(piles)
while lo < hi:
mid = (lo + hi) // 2
if hours_needed(mid) <= hours:
hi = mid # feasible — try smaller
else:
lo = mid + 1 # too slow
return lo
The signal for this pattern: "minimize the maximum," "maximize the minimum," or "find the smallest capacity/speed/size such that something is possible."
Problems: Binary Search · Search a 2D Matrix · Koko Eating Bananas · Find Minimum in Rotated Sorted Array · Search in Rotated Sorted Array · Median of Two Sorted Arrays · Split Array Largest Sum · Capacity to Ship Packages in D Days · Time-Based Key-Value Store
7. Recursion and backtracking
Learn: base case + recursive case, the decision tree, choose→explore→un-choose, pruning.
The template that covers most backtracking problems:
def subsets(nums):
result, path = [], []
def backtrack(start):
result.append(path[:]) # copy — path keeps mutating
for i in range(start, len(nums)):
path.append(nums[i]) # choose
backtrack(i + 1) # explore
path.pop() # un-choose
backtrack(0)
return result
def permutations(nums):
result = []
def backtrack(path, remaining):
if not remaining:
result.append(path[:])
return
for i in range(len(remaining)):
backtrack(path + [remaining[i]], remaining[:i] + remaining[i+1:])
backtrack([], nums)
return result
Three questions to ask yourself for any backtracking problem: What's a partial solution? What choices extend it? When do I stop? Answer those and the code writes itself.
Complexity here is usually exponential and that's expected: subsets are O(n·2ⁿ), permutations O(n·n!). Say the number out loud; interviewers want to know you understand the cost.
Problems: Subsets · Subsets II · Combination Sum · Permutations · Word Search · Palindrome Partitioning · Letter Combinations of a Phone Number · N-Queens · Sudoku Solver · Generate Parentheses
8. Trees
Learn: binary trees, BSTs, all four traversals, recursive vs iterative, BFS by level, tree DP.
Trees are the highest-frequency topic in real interviews after arrays and hashing. The good news: 80% of tree problems are one of three templates.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val, self.left, self.right = val, left, right
# Template 1: recursive DFS — "solve for children, combine"
def max_depth(root):
if not root:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))
def is_balanced(root):
def height(node):
if not node:
return 0
lh, rh = height(node.left), height(node.right)
if lh == -1 or rh == -1 or abs(lh - rh) > 1:
return -1 # sentinel signals "unbalanced" upward
return 1 + max(lh, rh)
return height(root) != -1
# Template 2: BFS by level — anything about levels, widths, or shortest paths
from collections import deque
def level_order(root):
if not root:
return []
result, queue = [], deque([root])
while queue:
level = []
for _ in range(len(queue)): # snapshot the size: this is one level
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
# Template 3: BST property — inorder is sorted, and comparisons prune half the tree
def validate_bst(root):
def check(node, low, high):
if not node:
return True
if not (low < node.val < high):
return False
return check(node.left, low, node.val) and check(node.right, node.val, high)
return check(root, float("-inf"), float("inf"))
Problems: Invert Binary Tree · Maximum Depth · Same Tree · Subtree of Another Tree · Lowest Common Ancestor of a BST · Level Order Traversal · Validate BST · Kth Smallest Element in a BST · Construct Tree from Preorder and Inorder · Binary Tree Maximum Path Sum · Serialize and Deserialize Binary Tree · Diameter of Binary Tree
9. Heaps and priority queues
Learn: heapq, min vs max heaps, top-k, two-heap median, merging sorted streams.
Recognize it when: you need repeated access to the smallest or largest element, or "top k" / "kth largest" / "median of a stream."
def k_closest_to_origin(points, k):
"""O(n log k) with a size-k max-heap, better than O(n log n) sorting."""
import heapq
heap = []
for x, y in points:
heapq.heappush(heap, (-(x*x + y*y), x, y)) # negate → max-heap
if len(heap) > k:
heapq.heappop(heap) # evict the farthest
return [[x, y] for _, x, y in heap]
class MedianFinder:
"""Two heaps: max-heap for the lower half, min-heap for the upper half."""
def __init__(self):
self.low, self.high = [], [] # low is negated
def add(self, num):
import heapq
heapq.heappush(self.low, -num)
heapq.heappush(self.high, -heapq.heappop(self.low)) # balance
if len(self.high) > len(self.low):
heapq.heappush(self.low, -heapq.heappop(self.high))
def median(self):
if len(self.low) > len(self.high):
return -self.low[0]
return (-self.low[0] + self.high[0]) / 2
Problems: Kth Largest Element in an Array · Top K Frequent Elements · Find Median from Data Stream · Merge K Sorted Lists · Task Scheduler · K Closest Points to Origin · Last Stone Weight · Design Twitter
10. Graphs
Learn: adjacency list representation, BFS, DFS, cycle detection, topological sort, union-find, Dijkstra.
Represent graphs as defaultdict(list) unless told otherwise. And remember: a 2D grid is a graph, where neighbors are the four adjacent cells. Recognizing that turns half of all grid problems into standard BFS/DFS.
from collections import defaultdict, deque
# ---------- BFS: shortest path in an unweighted graph ----------
def bfs_shortest(graph, start, goal):
queue = deque([(start, 0)])
visited = {start}
while queue:
node, dist = queue.popleft()
if node == goal:
return dist
for nxt in graph[node]:
if nxt not in visited:
visited.add(nxt) # mark on ENQUEUE, not dequeue
queue.append((nxt, dist + 1))
return -1
# ---------- Grid DFS: connected components ----------
def count_islands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
def sink(r, c):
if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != "1":
return
grid[r][c] = "0" # mark visited in place
for dr, dc in ((1,0), (-1,0), (0,1), (0,-1)):
sink(r + dr, c + dc)
islands = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == "1":
islands += 1
sink(r, c)
return islands
# ---------- Topological sort (Kahn's): dependency ordering ----------
def topo_sort(n, prerequisites):
graph = defaultdict(list)
indegree = [0] * n
for course, prereq in prerequisites:
graph[prereq].append(course)
indegree[course] += 1
queue = deque(i for i in range(n) if indegree[i] == 0)
order = []
while queue:
node = queue.popleft()
order.append(node)
for nxt in graph[node]:
indegree[nxt] -= 1
if indegree[nxt] == 0:
queue.append(nxt)
return order if len(order) == n else [] # short result ⇒ there's a cycle
# ---------- Union-Find: connectivity, cycle detection, Kruskal's MST ----------
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [1] * n
self.components = n
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]] # path compression
x = self.parent[x]
return x
def union(self, a, b):
ra, rb = self.find(a), self.find(b)
if ra == rb:
return False # already connected → cycle
if self.rank[ra] < self.rank[rb]:
ra, rb = rb, ra
self.parent[rb] = ra
self.rank[ra] += self.rank[rb]
self.components -= 1
return True
# ---------- Dijkstra: shortest path with weights ----------
def dijkstra(graph, start):
import heapq
dist = {start: 0}
heap = [(0, start)]
while heap:
d, node = heapq.heappop(heap)
if d > dist.get(node, float("inf")):
continue # stale entry
for nxt, weight in graph[node]:
nd = d + weight
if nd < dist.get(nxt, float("inf")):
dist[nxt] = nd
heapq.heappush(heap, (nd, nxt))
return dist
Problems: Number of Islands · Clone Graph · Max Area of Island · Pacific Atlantic Water Flow · Surrounded Regions · Rotting Oranges · Course Schedule · Course Schedule II · Redundant Connection · Number of Connected Components · Graph Valid Tree · Word Ladder · Network Delay Time · Cheapest Flights Within K Stops · Alien Dictionary
11. Dynamic programming
Learn: memoization vs tabulation, state definition, transitions, the classic families.
DP is where most candidates struggle, and the reason is almost always that they try to memorize solutions instead of learning the process. The process:
- Can this be solved by making a sequence of choices, where subproblems repeat? If yes, DP is on the table.
- Define the state. What minimal information identifies a subproblem? This is the hard step. Write it as an English sentence first: "
dp[i]= the length of the longest increasing subsequence ending at index i." - Write the recurrence. How does the answer at this state depend on smaller states?
- Identify base cases.
- Write it top-down with
@cachefirst — it's closer to the recursive intuition and harder to get wrong. - Convert to bottom-up if asked, then optimize space if only the last row or two is needed.
from functools import cache
# Step 5 in practice: brute-force recursion + one decorator = DP
@cache
def climb(n):
if n <= 2:
return n
return climb(n - 1) + climb(n - 2)
# Bottom-up with O(1) space
def climb_iterative(n):
a, b = 1, 2
for _ in range(n - 2):
a, b = b, a + b
return b if n > 1 else 1
# ---------- 0/1 knapsack: the archetype ----------
def knapsack(weights, values, capacity):
dp = [0] * (capacity + 1) # dp[c] = best value with capacity c
for w, v in zip(weights, values):
for c in range(capacity, w - 1, -1): # iterate DOWN so each item is used once
dp[c] = max(dp[c], dp[c - w] + v)
return dp[capacity]
# ---------- Longest common subsequence: the 2D archetype ----------
def lcs(a, b):
dp = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
for i in range(1, len(a) + 1):
for j in range(1, len(b) + 1):
if a[i-1] == b[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[-1][-1]
# ---------- Longest increasing subsequence ----------
def lis(nums):
"""O(n log n) using patience sorting + bisect."""
import bisect
tails = []
for n in nums:
i = bisect.bisect_left(tails, n)
if i == len(tails):
tails.append(n)
else:
tails[i] = n
return len(tails)
The DP families to recognize:
| Family | State shape | Examples |
|---|---|---|
| Linear / Fibonacci-like | dp[i] |
Climbing Stairs, House Robber, Decode Ways |
| Knapsack (0/1 and unbounded) | dp[i][capacity] |
Partition Equal Subset Sum, Coin Change, Target Sum |
| Two sequences | dp[i][j] |
LCS, Edit Distance, Regex Matching |
| Grid | dp[r][c] |
Unique Paths, Minimum Path Sum |
| Intervals | dp[i][j] over ranges |
Burst Balloons, Matrix Chain, Palindrome Partitioning II |
| Subsequence | dp[i] + inner loop |
LIS, Longest Palindromic Subsequence |
| Bitmask | dp[mask] |
Travelling Salesman, Partition to K Subsets |
| State machine | dp[i][state] |
Best Time to Buy/Sell Stock with Cooldown / Fee / K Transactions |
Problems (in this order): Climbing Stairs · Min Cost Climbing Stairs · House Robber · House Robber II · Coin Change · Longest Palindromic Substring · Palindromic Substrings · Decode Ways · Maximum Product Subarray · Word Break · Longest Increasing Subsequence · Partition Equal Subset Sum · Unique Paths · Longest Common Subsequence · Edit Distance · Best Time to Buy and Sell Stock with Cooldown · Coin Change II · Target Sum · Interleaving String · Burst Balloons · Regular Expression Matching
12. Greedy and intervals
Learn: exchange arguments, sorting as a preprocessing step, interval merging and scheduling.
Greedy is the pattern where a locally optimal choice yields a global optimum. The risk is that greedy feels right and is wrong. If you propose a greedy solution, be prepared to justify why the local choice is safe — or to acknowledge you'd want to prove it. Interviewers respect "I think greedy works here because sorting by end time means we never block a better option, though I'd want to verify with a counterexample search" far more than unearned confidence.
def merge_intervals(intervals):
intervals.sort(key=lambda x: x[0]) # sort by start
merged = []
for start, end in intervals:
if merged and start <= merged[-1][1]: # overlaps the previous
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
def max_non_overlapping(intervals):
"""Classic activity selection: sort by END time, greedily take earliest finisher."""
intervals.sort(key=lambda x: x[1])
count, last_end = 0, float("-inf")
for start, end in intervals:
if start >= last_end:
count += 1
last_end = end
return count
def can_jump(nums):
"""Track the farthest reachable index."""
reach = 0
for i, n in enumerate(nums):
if i > reach:
return False
reach = max(reach, i + n)
return True
Problems: Maximum Subarray · Jump Game · Jump Game II · Gas Station · Merge Intervals · Insert Interval · Non-overlapping Intervals · Meeting Rooms · Meeting Rooms II · Minimum Number of Arrows · Partition Labels · Task Scheduler · Hand of Straights
13. Tries, bit manipulation, and math
Lower frequency, but they show up — and they're small enough to learn quickly.
Trie — prefix-based problems, autocomplete, word search on grids:
class TrieNode:
def __init__(self):
self.children = {}
self.is_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for ch in word:
node = node.children.setdefault(ch, TrieNode())
node.is_word = True
def search(self, word, prefix_only=False):
node = self.root
for ch in word:
if ch not in node.children:
return False
node = node.children[ch]
return True if prefix_only else node.is_word
Bit manipulation — the operations worth knowing:
x & 1 # is odd?
x >> 1 # divide by 2
x << 1 # multiply by 2
x ^ y # XOR: a ^ a == 0, a ^ 0 == a → finds the unique element
x & (x - 1) # clears the lowest set bit
x & -x # isolates the lowest set bit
bin(x).count("1") # popcount
x.bit_count() # popcount, Python 3.10+
1 << i # the i-th bit as a mask
mask & (1 << i) # test bit i
mask | (1 << i) # set bit i
mask ^ (1 << i) # flip bit i
def single_number(nums):
"""Every element appears twice except one. O(n) time, O(1) space."""
from functools import reduce
import operator
return reduce(operator.xor, nums)
Math — GCD/LCM, primes via the Sieve of Eratosthenes, modular arithmetic (pow(base, exp, mod)), fast exponentiation, and combinatorics (math.comb).
Problems: Implement Trie · Design Add and Search Words · Word Search II · Single Number · Number of 1 Bits · Counting Bits · Reverse Bits · Missing Number · Sum of Two Integers · Pow(x, n) · Happy Number · Plus One · Count Primes
3.5 The 14-week interview study plan
Assumes ~12–15 hours per week alongside a job. If you have more time, compress; don't skip.
| Week | Focus | Target problems | Milestone |
|---|---|---|---|
| 1 | Complexity analysis, arrays, two pointers | 12 easy | Can state complexity for any code you write |
| 2 | Hashing, strings | 12 easy / 3 medium | Two Sum family solved instantly |
| 3 | Sliding window | 8 medium | Can derive the template from scratch |
| 4 | Stacks, queues, monotonic stack | 8 medium | Next-greater-element without hints |
| 5 | Linked lists | 10 medium | Reverse and merge from memory |
| 6 | Binary search + on the answer | 8 medium | Recognize "minimize the maximum" |
| 7 | Review week + first mock interview | Redo 15 previously failed problems | Honest gap list written down |
| 8 | Recursion, backtracking | 8 medium | Subsets/permutations template fluent |
| 9 | Trees part 1: DFS, BFS | 12 medium | All three tree templates from memory |
| 10 | Trees part 2: BST, tree DP, tries | 8 medium / 2 hard | Serialize/deserialize solved |
| 11 | Heaps, greedy, intervals | 10 medium | Top-k without sorting |
| 12 | Graphs: BFS/DFS/topo/union-find | 12 medium / 2 hard | Grid problems feel routine |
| 13 | Dynamic programming | 12 medium / 3 hard | Can define state for a new problem |
| 14 | Mocks, weak spots, behavioral prep | 5 mixed daily, timed | 3+ mock interviews completed |
Target totals: roughly 150–200 problems solved with genuine understanding. The distribution that matters: about 25% easy, 60% medium, 15% hard. Medium problems are where interviews actually live — most companies ask two mediums, not one hard.
The number people quote (300, 500, "all of LeetCode") is mostly signaling. 150 problems you can re-solve from scratch and explain beat 500 you pattern-matched once. Which brings us to the most important habit in this entire guide:
Keep a mistake log. One row per problem you failed or needed a hint on:
| Problem | Pattern | Why I missed it | Re-solve date |
|---|---|---|---|
| Coin Change | DP, unbounded knapsack | Defined state as "coins used" instead of "amount remaining" | +3 days, then +2 weeks |
Then actually re-solve them on those dates, from a blank file. This log is worth more than the next 100 new problems, because it targets your specific weaknesses rather than reinforcing what you already know. Almost nobody does this. It's the highest-return habit available to you.
3.6 How to practice each problem
A repeatable loop, roughly 35–50 minutes per problem:
- Read the problem and constraints. Use the constraints to guess the target complexity (see the table in 3.3).
- Set a timer for 20–25 minutes and think without coding. Write your approach in plain English or pseudocode. If you're stuck at 25 minutes, look at only a hint, not the solution.
- Code it. No autocomplete of full solutions, no AI. Type it.
- Test on paper before running. Trace one normal case and one edge case by hand. This builds the verification habit interviewers grade you on.
- Submit. If it fails, debug it yourself before reading anything.
- After solving, read the top solutions anyway. There is almost always a cleaner approach, and cleanliness is graded.
- State the complexity out loud. Time and space.
- Log it if you struggled.
- Re-solve from blank after 3 days and again after 2 weeks if it was hard.
Step 9 is the one everyone skips and the one that produces retention. Solving a problem once creates recognition; solving it again from scratch after a gap creates recall. Interviews test recall.
Part 4 — Executing in the Interview
This is the part with the highest return per hour invested, and the part almost everyone under-prepares. Two candidates with identical algorithmic ability routinely get different outcomes because one of them managed the 45 minutes well.
4.1 The framework: five phases, timed
For a 45-minute coding round:
| Phase | Time | What you do |
|---|---|---|
| 1. Clarify | 3–5 min | Restate the problem, ask about constraints and edge cases |
| 2. Examples | 2–3 min | Walk one concrete example; confirm your understanding matches theirs |
| 3. Approach | 5–8 min | Propose brute force, then optimize, state complexity, get buy-in before coding |
| 4. Code | 15–20 min | Write it while narrating |
| 5. Test & refine | 5–8 min | Trace a case, handle edges, discuss improvements |
The most common fatal error is skipping straight to phase 4. Coding a solution the interviewer hasn't agreed to means that if you've misread the problem, you burn 20 minutes before finding out. Getting buy-in costs 30 seconds.
Phase 1 — Clarify. What to ask.
Have these ready as a mental checklist so you're never silent at the start:
- "Let me make sure I have this right: given ___, I need to return ___. Is that correct?"
- "What's the expected size of the input?" (determines target complexity)
- "Can the input be empty or null?"
- "Are the values sorted? Can there be duplicates? Negatives?"
- "For strings — ASCII or Unicode? Case-sensitive?"
- "If there's no valid answer, what should I return?"
- "If there are multiple valid answers, is any one acceptable?"
- "Can I modify the input in place, or should I preserve it?"
- "Should I optimize for time or space if there's a tradeoff?"
Ask three or four of the relevant ones, not all nine. Reading the problem statement carefully answers some of them already, and asking about things the prompt clearly states reads as inattention.
Phase 3 — Approach. The script that works.
Say something structurally like this:
"The brute-force approach is to check every pair, which is O(n²) time and O(1) space. Given the constraint that n can be up to 10⁵, that's too slow. The bottleneck is re-scanning to find the complement. If I store what I've seen in a hash map, I can look up the complement in O(1), which gets me to O(n) time and O(n) space. Does that sound reasonable to start coding?"
That paragraph does five things at once: shows you can find a solution, shows you know it's insufficient and why, identifies the specific bottleneck, states the improvement with complexity, and invites collaboration. It is the single highest-value 30 seconds of the interview.
Always mention brute force first, even briefly, even if you immediately see the optimal solution. It demonstrates that your optimal solution is reasoned rather than memorized — which is exactly the doubt interviewers hold about candidates who've ground problems.
Phase 4 — Code. How to narrate.
Narrate intent, not syntax. Nobody wants "now I'm typing a for loop."
- Good: "I'll use a set to track the current window characters, so membership checks are O(1)."
- Bad: "So,
for,i,in,range,len,s..."
Other habits that read as senior:
- Write clean names.
left/right,seen,count,result— nota,b,x2,temp2. - Handle the guard clauses first.
if not nums: return []at the top signals edge-case awareness immediately. - If you need a helper, stub it and move on. "I'll assume a
is_valid(board)helper exists and implement it after the main logic." This keeps momentum and shows decomposition. - Say when you're unsure of an API. "I think
bisect_leftreturns the leftmost insertion point — I'd verify that in a real environment, but the logic holds either way." This is honesty, and it's scored positively. Confidently wrong is scored negatively.
Phase 5 — Test. What separates strong candidates.
Do not say "I'm done." Say "let me trace through this." Then actually do it — pick a small input and walk your variables line by line, out loud. When you find a bug this way, you've just demonstrated the single most valuable engineering trait there is.
Edge cases to check by reflex:
- empty input, single element, two elements
- all elements identical
- already-sorted and reverse-sorted input
- negatives and zero
- the maximum constraint (does it overflow, recurse too deep, or time out?)
- for trees: single node, one-sided/skewed tree
- for graphs: disconnected components, self-loops, cycles
Then close with a forward-looking sentence: "This is O(n) time and O(n) space. If memory were constrained, I could sort first and use two pointers for O(n log n) time and O(1) space instead."
4.2 What to do when you're stuck
You will get stuck. It's expected. How you get unstuck is itself the thing being evaluated. The failure mode is silence — an interviewer watching a candidate stare at a blank screen for four minutes has nothing to score.
An escalating sequence that works:
- Verbalize where you are. "I know I need to track the count of each character. What I'm not seeing yet is how to know when to shrink the window." Now the interviewer knows your exact blocker and can nudge you — that's what they're there for.
- Try a smaller case. Solve n=2 and n=3 by hand. Patterns emerge from concrete cases far more reliably than from staring at the abstraction.
- Run down your pattern list. Out loud: "Is this a sliding window? There's no contiguity requirement, so probably not. Could I sort first? That would let me use two pointers..."
- Ask what's redundant. Most optimizations come from eliminating repeated work. What am I computing more than once? Could I cache it, precompute it, or maintain it incrementally?
- Write the brute force. A working O(n²) solution scores far better than an unwritten O(n) one. Then optimize from working code.
- Take the hint gracefully. If offered one, say "that's helpful, thank you" and use it. Resisting help reads as rigid. Taking a hint costs you a little; refusing one and failing costs you the round.
4.3 Mock interviews are non-negotiable
Solving problems alone in your room prepares you for solving problems alone in your room. It does not prepare you for coding while a stranger watches, in a plain editor with no autocomplete, while explaining yourself.
Do at least five mock interviews before your first real one. Options: Pramp and interviewing.io (free peer mocks), a friend in the industry, a paid coach, or — the accessible version — record yourself solving a random problem out loud on a 45-minute timer and watch it back. Watching yourself is uncomfortable and unusually instructive; you'll notice the long silences, the filler, and the moments you started coding without a plan.
Practice in an environment like the real one: a plain text editor or a shared doc, no IDE, no autocomplete, no running the code. Write and reason without a safety net, because that's the actual condition.
4.4 The other rounds
Coding isn't the whole loop. A rough sense of the full picture:
| Round | Typical presence | What to prepare |
|---|---|---|
| Screen (recruiter) | Always | A crisp 90-second background summary; salary expectations; why this company |
| Online assessment | Common at large firms | Timed 2–4 problems; practice under time pressure specifically |
| Coding rounds | 1–4 rounds | Everything in Part 3 and this section |
| Python/language deep-dive | Common for Python-specific roles | See 4.5 below |
| System design | Mid-level and above | Scaling, databases, caching, queues, load balancing, tradeoffs |
| Behavioral | Always | 6–8 STAR stories |
| Hiring manager | Usually | Your motivations, questions about the team, project ownership |
Behavioral prep, briefly: prepare six to eight stories in STAR form (Situation, Task, Action, Result) covering: a conflict with a coworker, a failure and what you learned, a project you led, a time you received hard feedback, a time you had to learn something fast, a time you disagreed with a decision, and something you're proud of. Have real numbers in the Results where possible. Rehearse them out loud — they should take about two minutes each. The same six stories, recombined, answer nearly every behavioral question asked.
4.5 Python-specific questions you should expect
If you list Python as a strength, expect to be tested on the language and not just algorithms. Be able to answer these clearly:
- List vs tuple vs set vs dict — mutability, ordering, use cases, and which can be dict keys.
isvs==— identity versus equality. And why[] == []isTruebut[] is []isFalse.- Shallow vs deep copy —
copy.copyvscopy.deepcopy, and when nested structures bite you. - Mutable default arguments — why
def f(x=[])is a bug. - How dicts achieve O(1) — hashing, collision handling, why keys must be immutable, and that dicts have preserved insertion order since 3.7.
- Generators vs lists — laziness, memory, when you can't reuse a generator.
- Decorators — what
@somethingdesugars to, and whyfunctools.wrapsexists. *argsand**kwargs— and argument unpacking at the call site.- The GIL — what it is, why threads help I/O-bound but not CPU-bound work,
threadingvsmultiprocessingvsasyncio. - Context managers — what
withguarantees, and how to write one with__enter__/__exit__or@contextmanager. __init__vs__new__, and whatselfactually is.- Method resolution order — how Python resolves attributes across multiple inheritance.
- Duck typing and EAFP — "easier to ask forgiveness than permission": try the operation and catch the exception, rather than checking types first.
- Iterators vs iterables —
__iter__and__next__. @staticmethodvs@classmethod— and when each is appropriate.- List comprehension vs
map/filter— readability and performance. async/await— the event loop, and when concurrency helps.
If you can discuss the GIL, generators, and mutability with genuine understanding, you will sound more like an engineer than most candidates who've solved twice as many problems.
4.6 Résumé, projects, and GitHub
Interviews start before the interview.
Résumé: one page. Every bullet in the form action verb + what you did + measurable outcome. "Built an ETL pipeline in Python processing 2M records daily, cutting report generation from 6 hours to 12 minutes" beats "Responsible for data pipelines" by an enormous margin. List the specific tools; recruiters and automated systems both filter on keywords.
Projects: three good ones beat ten tutorial clones. A strong project has a real user need (even if the user is you), a README that shows what it does and how to run it, tests, clean commit history, and — ideally — deployment so it can actually be clicked. Avoid the tutorial trilogy of to-do app, weather app, and calculator unless you've taken them somewhere unusual.
GitHub: pin your best three. Make sure every pinned repo has a real README with a screenshot or example output. A repo with no README reads as abandoned.
Be able to defend everything you list. If your résumé says Docker, expect "walk me through your Dockerfile." If a project used a library, expect "why that one and not the alternative?" One unanswerable claim casts doubt on the entire document.
4.7 The final week, the day before, the day of
Final week: stop learning new topics. Review your mistake log, re-solve 20 problems you previously failed, rehearse your STAR stories out loud, and do two mocks. New material this late adds anxiety, not capability.
Day before: one light review session, maximum two hours. Prepare your setup — test your camera, microphone, and internet; have water and paper nearby; know the editor. Prepare three questions to ask the interviewer about the team and the work. Then stop and sleep properly. Sleep affects working memory more than one additional day of study affects your knowledge, and working memory is the resource an interview consumes.
Day of: eat something. Arrive or log in five minutes early. Have paper for diagramming. Then, during the interview: slow down. Nearly every candidate rushes. Reading the problem twice costs 20 seconds and prevents the most expensive mistake available to you.
Afterward: write down every question you were asked and every place you struggled, within an hour, while it's fresh. Whether you get the offer or not, that record is the most accurate study guide you will ever have for the next loop.
4.8 On rejection
You will be rejected, probably several times, possibly for reasons unrelated to your ability — headcount freezes, an internal candidate, a slightly stronger applicant, or an interviewer having a bad day. The process is noisy. Strong engineers get rejected by companies that later hire weaker ones.
Two things to do with it: ask for feedback (you'll rarely get specifics, but occasionally you'll get something valuable), and log the questions that beat you. Then apply again. Most companies allow reapplication after 6–12 months, and candidates who return with visible improvement are viewed favorably.
Don't treat one interview as a verdict on your competence. It's a sample from a distribution, and you control the distribution by continuing to practice and by taking more samples.
Appendix A — Quick reference: pattern → signal
Print this. It's the fastest way to build the recognition reflex.
| If the problem says... | Reach for |
|---|---|
| Sorted array, find a pair/triple | Two pointers |
| Contiguous subarray/substring with a condition | Sliding window |
| "Find duplicates," "have I seen this," count frequency | Hash map / set / Counter |
| Top k, kth largest, median of a stream | Heap |
| Repeated range sums | Prefix sum |
| Next greater/smaller element, histogram | Monotonic stack |
| Matching pairs, nesting, undo, expression parsing | Stack |
| "Shortest path" in an unweighted graph or grid | BFS |
| Explore all paths, connected components, flood fill | DFS |
| Shortest path with weights | Dijkstra |
| Dependencies, ordering, "can this be completed" | Topological sort |
| Connectivity, "are these in the same group," cycle in undirected | Union-Find |
| Sorted input or monotonic answer space | Binary search |
| "Minimize the maximum" / "maximize the minimum" | Binary search on the answer |
| All combinations/permutations/subsets, constraint satisfaction | Backtracking |
| Count ways, min/max over sequential choices, overlapping subproblems | Dynamic programming |
| Prefix matching, autocomplete, dictionary of words | Trie |
| Overlapping ranges, scheduling | Sort by start or end + greedy |
| Find the unique element, "O(1) space" with pairs | XOR / bit manipulation |
| Linked list, "without extra space" | Fast/slow pointers, dummy head |
| Level-by-level, tree width, minimum depth | BFS with a level snapshot |
| In-order gives sorted, search prunes half | BST properties |
| n ≤ 20, subsets of a set as state | Bitmask DP |
Appendix B — The consolidated resource list
Learning Python: the official docs tutorial · Automate the Boring Stuff (free) · CS50P (free) · Real Python · Fluent Python (intermediate+) · Python Tricks (Dan Bader)
Practice: Exercism (mentored) · Advent of Code · Codewars · HackerRank
Interview prep: NeetCode 150 / NeetCode roadmap (the best-curated free problem list) · LeetCode · Blind 75 · Grokking the Coding Interview (pattern-based) · Cracking the Coding Interview (dated on specifics, still good on process) · Elements of Programming Interviews in Python · Tech Interview Handbook (free, excellent on the non-coding parts)
Mocks: Pramp · interviewing.io · a friend · a recording of yourself
Systems and design: Designing Data-Intensive Applications · System Design Primer (free on GitHub) · High Scalability
Staying current: Real Python and Talk Python podcasts · the Python Discord · r/learnpython for questions, r/Python for news
Appendix C — Frequently asked questions
How long until I can get a job? With no programming background and consistent effort: 8–14 months to interview-ready. With an existing programming background: 3–5 months. With a CS degree needing interview prep only: 2–3 months. These are honest medians, not floors — some people take longer and still succeed.
Is Python a good choice for coding interviews? Yes, and it's arguably the best. Less code means less time and fewer bugs under pressure, and the standard library gives you heaps, deques, counters, and caching for free. The only caveats: a handful of companies specify a language (usually for infrastructure roles), and Python's slowness can occasionally cause a timeout on a solution that would pass in C++ — rare, and usually a sign your complexity is wrong rather than your language.
How many problems do I need to solve? 150–200 solved with real understanding, weighted toward mediums. Quality dominates quantity past that point. If you can't re-solve a problem from blank three weeks later, it doesn't count toward your total.
Should I use AI assistants while learning? Yes, for explanation, critique, and quizzing. No, for writing code you'll claim as your own or for skipping the struggle. The struggle is not an obstacle to learning; it is the learning. And you'll be asked to explain, in real time, code you can't have generated in the room.
Do I need a CS degree? No, but you need to know what a CS degree teaches: data structures, algorithms, complexity, some operating systems and networking basics, and databases. Self-taught engineers get hired constantly. They just can't skip the fundamentals and expect to pass a technical loop.
Is LeetCode grinding actually useful, or is it just hazing? Partly both. It's an imperfect proxy that over-weights pattern recall. But it does build real skills — decomposition, complexity reasoning, careful implementation — and it's the filter that exists, so treating it as a game to be understood rather than an injustice to be resented is the more useful stance.
Should I learn another language too? Not until Python is solid and you have a job or offers. Then yes — a second language (Go, Rust, TypeScript, or C++, depending on your track) deepens your understanding of the first by showing you which of Python's behaviors are universal and which are choices.
What if I forget everything I've learned? You won't forget the important parts if you built things and kept a mistake log. And you don't need to hold it all in memory — you need to hold the patterns and know where to look up the rest. That's what professional programming actually is.
The short version
If you take four things from this guide:
- Build things. Reading and watching create the illusion of competence. Only writing code that fails and then works creates the real thing.
- Learn patterns, not problems. Fifteen patterns generalize; four hundred memorized solutions don't.
- Keep a mistake log and actually re-solve from it. This is the highest-return habit in the whole document and almost nobody does it.
- Practice talking while you code. Interviews score communication and verification, not just correctness — and this is where equally capable candidates diverge.
Start today, with something small. Install Python, create a virtual environment, and write a program that does one thing you find slightly annoying. That's the whole beginning.