Python interviewers test reasoning, not rote syntax: mutability, GIL, decorators, and generators appear in every senior round
Mutable default args cause shared-state bugs — use None and create fresh defaults inside the function
The GIL serialises CPU-bound threads — use multiprocessing for parallelism, threading for I/O
Decorators wrap functions for cross-cutting concerns; @functools.wraps preserves metadata
Generators yield lazily — use them for streaming data or infinite sequences to save memory
Context managers (with statement) acquire/release resources cleanly; implement with __enter__/__exit__ or @contextmanager
✦ Definition~90s read
What is Top 50 Python Interview Questions?
Python's mutable default arguments are a classic gotcha where a function's default parameter value—typically a list, dict, or set—is evaluated once at definition time, not each call. This means every invocation shares the same mutable object, so mutations persist across calls, effectively creating a data leak.
★
Think of a Python interview like a driving test.
The core problem is that Python binds default arguments to the function object itself, stored in __defaults__, and mutating them modifies that shared state. This violates the principle of least surprise and can cause subtle, hard-to-debug bugs in production code, especially in long-running services or when the function is called from multiple contexts.
The fix is trivial: use None as the default and instantiate the mutable inside the function body. This gotcha is a favorite interview question because it tests understanding of Python's object model, scoping rules, and the distinction between definition-time and runtime evaluation.
It also ties directly into memory management—since the default object's identity remains constant—and concurrency, where shared mutable state across calls can race under the GIL. Beyond the gotcha itself, the article explores how this pattern relates to generators (which lazily evaluate and can also leak state via closures), decorators (which wrap functions and may inadvertently capture mutable defaults), and context managers (which manage resources but can suffer similar state-sharing issues if not implemented carefully).
Understanding this single concept sharpens your mental model of Python's execution semantics and helps you write safer, more predictable code.
Plain-English First
Think of a Python interview like a driving test. The examiner doesn't just want to see you turn the wheel — they want to know you understand WHY you check mirrors, signal, and brake in that order. These 50 questions are the 'manoeuvres' every Python examiner tests. Know the reasoning behind each one and you'll pass confidently, not by luck.
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
Python interviews trip up even experienced developers — not because the language is hard, but because interviewers aren't just testing syntax. They're testing whether you understand memory management, mutability traps, the CPython internals that explain quirky behaviour, and whether you can apply the right tool for the right job under pressure. A candidate who can recite list comprehension syntax but can't explain WHY it's faster than a for-loop won't get the role.
This article exists because most interview prep resources give you a list of questions with shallow one-line answers. That's fine for a quiz, but it won't help you when the interviewer follows up with 'why does that happen?' or 'what would you use instead in a production system?' Every answer here explains the mechanism, not just the result — so you can handle follow-ups confidently.
By the end of this article you'll be able to answer all 50 questions with depth, spot the common traps interviewers deliberately set, write clean Python that demonstrates seniority, and walk into any intermediate-to-senior Python interview with genuine confidence rather than crossed fingers.
Why Mutable Default Arguments Are a Data Leak
In Python, default arguments are evaluated once at function definition time, not each time the function is called. This means if you use a mutable object like a list or dict as a default, that single object is shared across all calls. The core mechanic: the default value is bound to the function object itself, stored in __defaults__, and reused every invocation.
When you mutate that default argument inside the function — appending to a list, adding to a dict — the change persists across calls. This is not a bug; it's how Python's object model works. The function signature is compiled once, and the default objects live as long as the function exists. This behavior is deterministic and reproducible, but it violates the assumption most developers make that each call gets a fresh default.
Use this pattern intentionally for caching, memoization, or accumulating state across calls — but never for a parameter that should start empty each time. In real systems, this gotcha silently corrupts data: a list that should be empty for each request accumulates entries from previous requests, leading to hard-to-debug state leaks. The fix is to use None as the default and create a fresh mutable inside the function body.
⚠ Common Misconception
The default is not re-evaluated on each call. It's a single object created at def time, shared by all invocations.
📊 Production Insight
A web handler using a mutable default list to accumulate query parameters will mix data across concurrent requests, causing one user to see another user's data.
The symptom: intermittent, non-reproducible data corruption that disappears when you add debug prints (because timing changes).
Rule: never use a mutable default for a parameter that should be fresh per call — use None and instantiate inside the function.
🎯 Key Takeaway
Default arguments are evaluated once at function definition, not at call time.
Mutating a default argument mutates the shared object, leaking state across calls.
Use None as the default for mutable parameters and create a new instance inside the function body.
thecodeforge.io
Top Python Interview Questions
Memory Management: Mutability and Identity
One of the most frequent 'gotcha' questions in Python interviews concerns the difference between mutable and immutable objects. In Python, everything is an object. Immutable objects (like strings, ints, and tuples) cannot be changed after creation; any 'modification' actually creates a new object in memory. Mutable objects (like lists and dicts) can be changed in place. This distinction is critical when passing arguments to functions, as Python uses 'Pass-by-Object-Reference.'
Type this code yourself rather than copy-pasting. The muscle memory of writing it will help it stick. In interviews, always mention that default arguments in Python are evaluated at definition time, not execution time.
📊 Production Insight
Mutability defaults cause silent data corruption that surfaces weeks later.
Only way to debug is to print(id(mylist)) across calls — painful.
Rule: never use mutable literals as defaults; use None and instantiate inside.
🎯 Key Takeaway
Default args are evaluated once at function definition.
Mutable defaults are shared across all calls.
Always use None as sentinel and create fresh mutable inside.
Concurrency and the GIL (Global Interpreter Lock)
For senior roles, you must explain why Python threads don't speed up CPU-bound tasks. The Global Interpreter Lock (GIL) is a mutex that protects access to Python objects, preventing multiple native threads from executing Python bytecodes at once. While this makes memory management simpler, it means for true parallelism in CPU-intensive work, you must use the multiprocessing module to bypass the GIL by using separate memory spaces.
# io.thecodeforge: Parallelism via Multiprocessingfrom multiprocessing importPoolimport os
defcpu_intensive_task(n):
# Simulate heavy computationreturnsum(i * i for i inrange(n))
if __name__ == "__main__":
numbers = [10**6, 10**6, 10**6, 10**6]
withPool(processes=os.cpu_count()) as pool:
results = pool.map(cpu_intensive_task, numbers)
print(f"Task completed across {os.cpu_count()} cores.")
Output
Task completed across 8 cores.
💡Strategy Note:
If the interviewer asks about I/O-bound tasks (like web scraping), emphasize that threading is still useful because the GIL is released during I/O operations.
📊 Production Insight
Using threading for CPU work causes performance worse than single-threaded.
The GIL context-switching overhead kills throughput.
Rule: threading for I/O, multiprocessing for CPU, asyncio for both when I/O dominates.
🎯 Key Takeaway
GIL lets one thread run Python bytecode at a time.
Threading adequate for I/O — GIL released during blocking calls.
Multiprocessing gives true parallelism via separate processes.
thecodeforge.io
Top Python Interview Questions
Generators and Iterators: Lazy Evaluation in Action
Generators are functions that use yield to produce a sequence of values lazily. They pause execution at each yield, preserving state until the next value is requested via next(). This makes them memory-efficient for large datasets and infinite sequences. Iterators are objects that implement __iter__() and __next__(). Python's for loop works by calling iter() on the target and then next() repeatedly until StopIteration is raised. Generators are a special case of iterators generated by functions with yield.
# io.thecodeforge: Custom iterator vs generator for debouncing eventsclassEventTimestampIterator:
def__init__(self, timestamps):
self._timestamps = timestamps
self._index = 0def__iter__(self):
returnselfdef__next__(self):
ifself._index >= len(self._timestamps):
raiseStopIteration
ts = self._timestamps[self._index]
self._index += 1return ts
# Generator equivalent – more concisedefdebounced_events(timestamps, interval_ms=500):
previous = Nonefor ts in timestamps:
if previous isNoneor (ts - previous) >= interval_ms:
previous = ts
yield ts
# Both produce the same output but generator uses less boilerplate
Mental Model
Mental Model: Generators as Paused Functions
Imagine a function that can freeze mid-execution and resume where it left off, packing its local variables into a backpack until called again.
yield freezes the function frame and returns a value to the caller.
The generator object stores the frame and local state on the heap.
Calling next() un-freezes and continues until the next yield or end.
This is exactly how async/await works in Python — it's syntactic sugar over generators.
📊 Production Insight
Reading a 10GB file line by line with a generator works; loading it all into a list crashes.
Always prefer generators for streams, logs, and database cursors.
Rule: if you iterate once, use a generator. If you need random access, use a list.
🎯 Key Takeaway
Generators produce items on demand — memory footprint is O(1).
They maintain state across yield points without extra class code.
Use them for any sequence that could grow unbounded.
Decorators: Wrapping Functions for Cross-Cutting Concerns
A decorator is a function that takes another function as argument, extends its behaviour, and returns a new function. Python's @decorator syntax is syntactic sugar for func = decorator(func). Common use cases: logging, authentication, caching, timing, validation. Critical detail: unless you use @functools.wraps, the decorated function loses its original __name__ and __doc__, which breaks introspection and tooling.
# io.thecodeforge: Production-grade timing decorator with @wrapsimport functools
import time
defmeasure_time(func):
@functools.wraps(func)
defwrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@measure_time
deffetch_user_data(user_id):
time.sleep(0.2) # simulate DB callreturn {"id": user_id, "name": "Alice"}
print(fetch_user_data(1))
print(f"Function name preserved: {fetch_user_data.__name__}")
Output
fetch_user_data took 0.2001s
{'id': 1, 'name': 'Alice'}
Function name preserved: fetch_user_data
⚠ Common Mistake: Missing @wraps
Without @functools.wraps, fetch_user_data.__name__ becomes 'wrapper'. This breaks tools like pytest, logging, and debuggers that rely on function identities.
📊 Production Insight
Decorators without @wraps cause silent failures in middleware routing and test fixtures.
Always apply @wraps to the inner function — it copies __name__, __doc__, __module__.
Rule: any decorator you write for production must include @functools.wraps.
🎯 Key Takeaway
Decorators add pre/post processing without modifying the original function.
@functools.wraps preserves function identity — never skip it.
Use decorators for cross-cutting concerns only; don't overuse them for business logic.
Context Managers and Resource Management
Context managers handle acquiring and releasing resources automatically using the with statement. Built-in examples: file I/O, database connections, locks. You can create custom context managers by defining __enter__ and __exit__ methods on a class, or by decorating a generator function with @contextlib.contextmanager. The __exit__ method receives exception type, value, and traceback — you can choose to suppress exceptions or log them.
# io.thecodeforge: Context manager for database sessionsfrom contextlib import contextmanager
@contextmanager
defdb_session(connection_string):
conn = create_connection(connection_string) # not showntry:
yield conn
conn.commit() # commit if no exceptionexceptException:
conn.rollback()
raisefinally:
conn.close()
# Usage — resource is automatically closedwithdb_session("postgresql://localhost/mydb") as conn:
conn.execute("INSERT INTO users ...")
💡Interview Hook:
When the interviewer asks 'How would you implement a context manager?', start with the class-based approach (__enter__/__exit__), then mention the @contextmanager shortcut for simple cases. This shows depth.
📊 Production Insight
A missing try/finally around explicit close() calls leaks database connections.
Context managers eliminate that class of bug entirely.
Rule: any function that acquires a resource must use with statement or be wrapped in one.
🎯 Key Takeaway
Context managers guarantee cleanup even when exceptions occur.
__exit__ can suppress exceptions by returning True — only do that intentionally.
Use @contextmanager for simple cases; class-based for complex state.
Pass by Object Reference: Why Your Function Mutates the Caller's Data
Junior devs love to argue pass-by-value vs pass-by-reference. The truth is messier: Python uses pass-by-object-reference. The variable name you pass is a reference to an object. If you assign a new value to that name inside the function, the caller doesn't see it. But if you mutate the object the name points to, the caller sees the change. This is why list.append() modifies the original list, but list = new_list does not. Understanding this prevents production bugs where a function unexpectedly corrupts a shared cache or database session state. Always copy mutable objects before modifying them unless you explicitly want side effects.
mutation_trap.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge
defadd_item(item, target_list=[]):
target_list.append(item)
return target_list
# Production trap: default list is shared across callsprint(add_item('a')) # ['a']print(add_item('b')) # ['a', 'b'] -- not ['b']!# Safe version: use immutable sentinelfrom typing importList, Optionaldefadd_item_safe(item: str, target_list: Optional[List[str]] = None) -> List[str]:
if target_list isNone:
target_list = []
target_list.append(item)
return target_list
Output
['a']
['a', 'b']
['a']
['b']
⚠ Production Trap:
Never use mutable default arguments like def foo(items=[]). Every call without that argument shares the same list object. Use None and create a new list inside the function.
🎯 Key Takeaway
In Python, your function receives the object reference, not a copy of the value. Mutate the object, and the caller sees it. Reassign the name, and the caller doesn’t.
thecodeforge.io
Top Python Interview Questions
The `__slots__` Directive: Slash Memory Usage Without Changing Logic
Every Python object carries a __dict__ that stores its attributes in a hash map. For thousands of small objects, this overhead kills memory and cache locality. __slots__ tells the interpreter to allocate a fixed-size array for attributes instead of a dictionary. Each slot attribute becomes a descriptor, so attribute access is faster. Use it for data classes, ORM models, or any place you create many instances. The downside: you cannot add new attributes dynamically, and you must declare every attribute in the class hierarchy. Measure before optimizing, but when you have a million objects, __slots__ can cut memory from 50 bytes per instance to under 20.
slots_memory.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge
classPoint:
__slots__ = ('x', 'y')
def__init__(self, x: float, y: float):
self.x = x
self.y = y
# Try to add a new attribute at runtime
p = Point(1.0, 2.0)
# p.z = 3.0 # AttributeError: 'Point' object has no attribute 'z'# Memory comparison (rough)import sys
classDynamicPoint:
def__init__(self, x, y):
self.x = x
self.y = y
d = DynamicPoint(1, 2)
p = Point(1, 2)
print(f"Dynamic: {sys.getsizeof(d)} bytes + dict overhead")
print(f"Slotted: {sys.getsizeof(p)} bytes, no dict")
Output
Dynamic: 56 bytes + dict overhead
Slotted: 48 bytes, no dict
🔥When to Use:
Only apply __slots__ when profiling shows memory pressure from many instances. It breaks inheritance if a parent doesn't also define __slots__. Test your hierarchy.
🎯 Key Takeaway
__slots__ removes per-instance dictionaries, cutting memory by up to 50% for object-heavy code, at the cost of dynamic attribute creation.
● Production incidentPOST-MORTEMseverity: high
The Shared-State Disaster: Mutable Default Arguments
Symptom
Users get data from previous requests appended to their payload — personal data leaks occur intermittently.
Assumption
The developer assumed that the default argument my_list=[] creates a fresh list on every call, as it would in most languages.
Root cause
Python evaluates default arguments once at function definition time, not at call time. All calls share the same list object.
Fix
Replace mutable defaults with None and create a new list inside the function if None is passed.
Key lesson
Never use mutable objects (list, dict, set) as default argument values.
Use None as the default and instantiate the mutable inside the function body.
Always check default arguments during code review — this bug is nearly invisible in log files.
Production debug guideFour common Python production failures and the exact commands to diagnose them4 entries
Symptom · 01
Memory usage grows without bound; process OOM-killed
→
Fix
Run tracemalloc.start() and snapshot; diff snapshots to find leak — or inspect with gc.get_objects()
Symptom · 02
Application hangs on a CPU-bound task
→
Fix
Attach with py-spy or strace to see which thread is spinning; check if you accidentally used threading instead of multiprocessing
Symptom · 03
Function returns stale data (mutable default bug)
→
Fix
Inspect the function's __defaults__ attribute; compare object IDs with id() to confirm shared state
Symptom · 04
ImportError after deployment: module not found
→
Fix
Run python -c 'import sys; print(sys.path)' and verify the module is in one of those paths; check for missing __init__.py
★ Quick Debug Cheat Sheet: Python Production IssuesFive high-signal commands for the most common Python interview topics that break in production
Recursion depth exceeded in production−
Immediate action
Increase recursion limit temporarily for diagnosis
Commands
import sys; sys.setrecursionlimit(5000)
sys.getrecursionlimit()
Fix now
Rewrite the algorithm iteratively or adjust the recursion limit carefully — but never rely on high limits in production.
GIL causing latency spikes in I/O-heavy service+
Immediate action
Swap threads for asyncio or multiprocessing
Commands
import asyncio; asyncio.run(async_main())
from multiprocessing import Pool; with Pool() as p: p.map(func, data)
Fix now
Use asyncio for high-concurrency I/O; use multiprocessing for CPU-bound parallel work.
Decorated function loses its name and docstring+
Immediate action
Check if @functools.wraps is applied
Commands
print(my_func.__name__) # returns 'wrapper' without wraps
Always apply @functools.wraps to the inner function of any decorator you write.
Generator ran out of items (StopIteration)+
Immediate action
Wrap usage in a for loop or next(gen, default)
Commands
next(gen, 'fallback')
list(gen) # careful: exhausts the generator
Fix now
Use for item in gen: loop which handles StopIteration automatically; never manually call next() without a default.
Context manager resource not released (file handle leak)+
Immediate action
Check if object implements __enter__/__exit__ or use contextlib.closing
Commands
type(obj).__exit__ # verify method exists
from contextlib import closing; with closing(thing):
Fix now
Always use the with statement for resources; if the object doesn't support it, wrap with contextlib.closing().
Feature
List
Tuple
Mutability
Mutable (Can change)
Immutable (Cannot change)
Memory Usage
Higher (Over-allocation for growth)
Lower (Fixed size)
Performance
Slower iteration
Faster iteration
Use Case
Homogeneous data, dynamic sizing
Heterogeneous data, fixed records/keys
⚙ Quick Reference
7 commands from this guide
File
Command / Code
Purpose
iothecodeforgecoremutability_demo.py
def append_to_list(val, my_list=[]): # DANGEROUS: Default args are evaluated on...
Memory Management
iothecodeforgeconcurrencygil_workaround.py
from multiprocessing import Pool
Concurrency and the GIL (Global Interpreter Lock)
iothecodeforgeiteratorsrange_simulator.py
class EventTimestampIterator:
Generators and Iterators
iothecodeforgedecoratorstiming_decorator.py
def measure_time(func):
Decorators
iothecodeforgecontext_managersdb_session.py
from contextlib import contextmanager
Context Managers and Resource Management
mutation_trap.py
def add_item(item, target_list=[]):
Pass by Object Reference
slots_memory.py
class Point:
The `__slots__` Directive
Key takeaways
1
You now understand that Python's 'Pass-by-Object-Reference' behaves differently for mutable vs. immutable types.
2
You've seen how to bypass the GIL using the multiprocessing module for CPU-bound performance.
3
Generators and context managers reduce memory footprint and ensure resource cleanup.
4
Decorators with @functools.wraps preserve function identity for production tooling.
5
The 'Forge' standard requires safe handling of default arguments to ensure production stability.
6
Practice daily
the forge only works when it's hot 🔥
Common mistakes to avoid
5 patterns
×
Using mutable default arguments in functions
Symptom
Function results unexpectedly accumulate data across calls; test suite fails when run in import order.
Fix
Replace default value [] or {} with None, then create the mutable inside the function body if None.
×
Confusing 'is' vs '==' equality
Symptom
Comparing two integer variables with 'is' works for small ints (caching) but fails unexpectedly for large ints or strings.
Fix
Use '==' for value equality, 'is' only for testing identity (e.g., x is None).
×
Failing to use context managers for file I/O or database connections
Symptom
ResourceWarning: unclosed file or database connection pool exhaustion in production.
Fix
Always use 'with open() as f:' or 'with db_session() as conn:' to guarantee cleanup.
×
Assuming threading speeds up CPU-bound tasks
Symptom
CPU utilisation remains low while threads contend for the GIL; throughput worse than single-threaded.
Fix
Use multiprocessing.Pool for CPU-bound work; reserve threading for I/O-bound tasks.
×
Forgetting @functools.wraps on decorators
Symptom
Decorated function appears with name 'wrapper' in logs and debugger; __doc__ lost.
Fix
Import functools, apply @functools.wraps to the inner function before returning it.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Explain the difference between `__str__` and `__repr__` in Python.
Q02SENIOR
How does Python's garbage collection handle reference cycles?
Q03JUNIOR
What is the difference between `@staticmethod` and `@classmethod`?
Q04SENIOR
Explain how Python resolves attribute lookups in classes (MRO).
Q05SENIOR
What is the purpose of `__slots__` and when should you use it?
Q01 of 05SENIOR
Explain the difference between `__str__` and `__repr__` in Python.
ANSWER
__repr__ aims to be unambiguous — ideally returning a string that could be used to recreate the object (e.g., Fraction(3, 4)). __str__ aims to be readable for end users. If only __repr__ is defined, __str__ falls back to it. Senior engineers always define __repr__ first, then __str__ when a pretty-print is needed.
Q02 of 05SENIOR
How does Python's garbage collection handle reference cycles?
ANSWER
Python uses reference counting as primary GC. When a reference cycle exists (e.g., two objects point to each other), their reference counts never drop to zero. The cyclic garbage collector, which runs periodically (triggered by allocation thresholds), detects these cycles by scanning objects with gc.garbage. It can break cycles if objects have __del__ methods — those objects go to gc.garbage and must be handled manually.
Q03 of 05JUNIOR
What is the difference between `@staticmethod` and `@classmethod`?
ANSWER
@staticmethod defines a method that does not receive the instance (self) or class (cls) as first argument — it's just a function living inside the class for namespace convenience. @classmethod receives the class as first argument (cls) and can be used to define alternative constructors or access class state. Use @classmethod when the method needs the class; use @staticmethod when it doesn't need the instance or class.
Q04 of 05SENIOR
Explain how Python resolves attribute lookups in classes (MRO).
ANSWER
Python uses C3 linearization to compute the Method Resolution Order (MRO). When you access obj.attr, Python first looks for attr in the instance's __dict__, then walks up the class hierarchy following the MRO of the class. For diamonds (multiple inheritance), C3 ensures monotonic ordering: child class MRO is consistent with all parents' MRO. You can view the MRO of any class via ClassName.__mro__.
Q05 of 05SENIOR
What is the purpose of `__slots__` and when should you use it?
ANSWER
__slots__ restricts a class to a fixed set of instance attributes, preventing dynamic attribute creation. This reduces memory overhead per instance (no per-instance __dict__). Use it in classes that will have millions of instances and a fixed set of attributes. Downside: you cannot add new attributes dynamically, and it breaks tools that rely on __dict__ (e.g., pickle by default).
01
Explain the difference between `__str__` and `__repr__` in Python.
SENIOR
02
How does Python's garbage collection handle reference cycles?
SENIOR
03
What is the difference between `@staticmethod` and `@classmethod`?
JUNIOR
04
Explain how Python resolves attribute lookups in classes (MRO).
SENIOR
05
What is the purpose of `__slots__` and when should you use it?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
What is the difference between a Deep Copy and a Shallow Copy in Python?
A shallow copy creates a new object but fills it with references to the original child objects. A deep copy creates a new object and recursively creates new copies of all child objects found in the original, ensuring total independence between the two structures.
Was this helpful?
02
How does Python's Garbage Collection work?
Python primarily uses Reference Counting. When an object's reference count drops to zero, it is immediately deallocated. To handle 'Reference Cycles' (where two objects point to each other), Python also employs a Generational Garbage Collector that periodically searches for and clears these cycles.
Was this helpful?
03
What are Python Decorators and when should you use them?
Decorators are functions that wrap another function to extend its behavior without permanently modifying it. They are best used for 'cross-cutting concerns' like logging, authentication, caching, or timing execution speed in a clean, reusable way.
Was this helpful?
04
Explain the difference between `yield` and `return` in a generator.
yield suspends the generator's execution and returns a value to the caller, preserving local state. The next call to next() resumes after the yield. return ends the generator and raises StopIteration. Python 3 allows return value in generators, which becomes the value of the StopIteration exception's .value attribute.
Was this helpful?
05
How do you handle large files that don't fit into memory in Python?
Read the file line-by-line using with open('large.txt') as f: for line in f: process(line). This reads one line at a time, using O(1) memory. For binary files, read in chunks with iter(lambda: f.read(8192), b''). For CSV, use csv.reader on a file object. Never use .read() or .readlines() on large files.