Python basics
How does Python run your code?
CPython, the standard interpreter, first compiles your source to bytecode and then runs that bytecode in its virtual machine. Python is dynamically typed, so a variable is a name bound to an object and the type is checked at runtime. Memory is managed for you.
What is the difference between mutable and immutable types?
A mutable object can change after it is created: list, dict and set. An immutable object cannot: int, float, str, tuple and frozenset. "Changing" an immutable value really creates a new object. Only immutable (hashable) values can be dict keys or set members.
What is the difference between a list and a tuple?
A list is mutable and is used for a collection that grows or changes. A tuple is immutable, slightly lighter, can be a dict key if its items are hashable, and is used for a fixed group of values such as coordinates or a function returning several values.
Why is looking up a key in a dict fast?
A dict is a hash table. Python hashes the key to find its slot directly, so lookup, insert and delete are O(1) on average, whatever the size. Since Python 3.7, dicts also keep insertion order.
When should you use a set?
Use a set when you need unique values or fast membership tests. x in a_set is O(1) on average, while x in a_list checks every item. Sets also support union, intersection and difference.
seen = set(["a", "b"])
print("a" in seen) # True, O(1)
print({1, 2, 3} & {2, 3, 4}) # {2, 3}
What is the difference between is and ==?
== checks whether two objects have equal values. is checks whether they are the same object in memory. Use is only for None (x is None); comparing numbers or strings with is depends on caching and gives surprising results.
a = [1, 2]
b = [1, 2]
print(a == b) # True
print(a is b) # False
What are *args and **kwargs?
*args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dict. The same stars unpack a list or dict when calling a function.
def log(*args, **kwargs):
print(args, kwargs)
log(1, 2, level="info") # (1, 2) {'level': 'info'}
Why is a mutable default argument a bug?
Default values are created once, when the function is defined, not on every call. A list default is therefore shared by every call and keeps growing. Use None and create the list inside the function.
def add(item, items=[]): # bug: one shared list
items.append(item)
return items
def add(item, items=None): # fix
if items is None:
items = []
items.append(item)
return items
What is a list comprehension?
A list comprehension builds a list in one readable expression, with an optional filter. It is usually faster than an equivalent for loop with append. Dict and set comprehensions work the same way.
squares = [n * n for n in range(10) if n % 2 == 0]
lengths = {word: len(word) for word in ["hi", "hello"]}
What is the difference between a shallow copy and a deep copy?
A shallow copy creates a new outer object but shares the inner objects, so changing a nested list affects both. A deep copy recursively copies everything. Use copy.deepcopy for nested structures.
import copy
a = [[1, 2], [3]]
b = a.copy() # shallow
c = copy.deepcopy(a) # deep
a[0].append(9)
print(b[0]) # [1, 2, 9]
print(c[0]) # [1, 2]
How does Python pass arguments to a function?
Python passes references to objects, sometimes called pass-by-object-reference or pass-by-assignment. A function can change a mutable object it receives, but assigning a new object to the parameter does not affect the caller.
How does slicing work?
seq[start:stop:step] returns a new sequence from start up to, but not including, stop. Negative indexes count from the end. seq[::-1] is the quick way to reverse a list or string.
s = "python"
print(s[1:4]) # "yth"
print(s[-2:]) # "on"
print(s[::-1]) # "nohtyp"
Functions, scope and decorators
What is a lambda?
A lambda is a small anonymous function written as one expression. It is handy as a sort key or a short callback. For anything longer, a named def is clearer.
users.sort(key=lambda u: u["age"])
What is a closure?
A closure is an inner function that remembers variables from the function that created it, even after that outer function has returned.
def multiplier(n):
def times(x):
return x * n # remembers n
return times
double = multiplier(2)
print(double(5)) # 10
What is a decorator?
A decorator is a function that takes a function and returns a new function with extra behaviour, such as logging, timing or access checks. @decorator above a def is shorthand for func = decorator(func). functools.wraps keeps the original name and docstring.
import functools, time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(func.__name__, time.perf_counter() - start)
return result
return wrapper
@timed
def work():
sum(range(10**6))
What is the LEGB rule?
It is the order in which Python looks up a name: Local (the current function), Enclosing (outer functions), Global (the module), then Built-in. The first match wins.
What is the difference between global and nonlocal?
Assigning to a name inside a function makes it local. global lets the function assign to a module-level variable. nonlocal lets an inner function assign to a variable of the enclosing function, which a closure counter needs.
What do map, filter and reduce do?
map applies a function to every item, filter keeps the items where a function returns true, and functools.reduce combines items into one value. They return iterators (except reduce). Comprehensions are often more readable for map and filter.
Object-oriented Python
What are __init__ and self?
__init__ initialises a new object after it is created. self is the object the method is called on; Python passes it automatically as the first argument, so obj.method(x) runs Class.method(obj, x).
What is the difference between class attributes and instance attributes?
A class attribute is shared by all instances. An instance attribute, set on self, belongs to one object. A mutable class attribute such as a list is a common bug, because every instance ends up changing the same list.
class Team:
members = [] # shared by all teams (bug)
def __init__(self):
self.players = [] # one list per team
What is the difference between @staticmethod and @classmethod?
A classmethod receives the class as its first argument (cls), which makes it the usual way to write alternative constructors. A staticmethod receives nothing automatically; it is a plain function kept inside the class for organisation.
class Date:
def __init__(self, y, m, d): ...
@classmethod
def from_string(cls, text):
return cls(*map(int, text.split("-")))
@staticmethod
def is_leap(year):
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
How does inheritance work, and what is the MRO?
A child class reuses and can override the parent's methods. With multiple inheritance, Python searches classes in the method resolution order (MRO), calculated with C3 linearisation; you can see it with ClassName.__mro__. super() follows the MRO, not just the direct parent.
What are dunder methods?
Methods with double underscores, such as __len__, __eq__ and __add__, let your objects work with built-in syntax. __repr__ should give an unambiguous developer view; __str__ gives the readable text print uses.
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
return Money(self.amount + other.amount)
def __repr__(self):
return f"Money({self.amount})"
What does @property do?
@property lets a method be read like an attribute, so you can add validation or calculation later without changing the code that uses the class. A matching setter controls assignment.
class Account:
def __init__(self):
self._balance = 0
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("negative balance")
self._balance = value
Does Python have private members?
Not enforced ones. A single underscore, _name, is a convention meaning "internal, do not use". A double underscore, __name, triggers name mangling to _ClassName__name, which avoids clashes in subclasses but is not real security.
Iterators, generators and errors
What is the difference between an iterable and an iterator?
An iterable is anything you can loop over, such as a list; its __iter__ returns an iterator. An iterator produces values one at a time with __next__ and raises StopIteration when done. An iterator can be used only once.
What is a generator?
A generator is a function that uses yield to produce values one at a time, pausing between them. It keeps only its current state in memory, so it can process huge files or endless sequences.
def read_lines(path):
with open(path) as f:
for line in f:
yield line.rstrip()
for line in read_lines("big.log"):
...
What is the difference between a generator expression and a list comprehension?
A list comprehension, in square brackets, builds the whole list in memory at once. A generator expression, in round brackets, produces items lazily. sum(x * x for x in range(10**7)) uses almost no memory; the list version stores ten million numbers.
What is a context manager?
A context manager sets something up and guarantees clean-up, used with the with statement. Files, locks and database connections use it so they are closed even if an exception happens. You can write one with __enter__ and __exit__, or with contextlib.contextmanager.
with open("data.txt") as f:
text = f.read()
# the file is closed here, even after an error
How does exception handling work in Python?
Code that may fail goes in try. except catches specific exception types. else runs only if no exception happened. finally always runs, for clean-up. Catch the narrowest exception you can; a bare except also hides bugs and Ctrl+C.
try:
value = int(text)
except ValueError:
value = 0
else:
print("parsed")
finally:
print("done")
How do you raise a custom exception?
Create a class that inherits from Exception, or from a more specific built-in exception, and raise it with a clear message. Callers can then catch your error type alone.
class PaymentFailed(Exception):
pass
raise PaymentFailed("card declined")
Common Python coding questions
How do you check whether a string is a palindrome?
Normalise the text, then compare it with its reverse. Interviewers often follow up by asking you to ignore spaces and punctuation, or to do it with two pointers.
def is_palindrome(text):
clean = [c.lower() for c in text if c.isalnum()]
return clean == clean[::-1]
print(is_palindrome("A man, a plan, a canal: Panama")) # True
How do you count how often each character appears?
collections.Counter counts every item in one pass and can return the most common ones directly. Without it, a plain dict with counts[ch] = counts.get(ch, 0) + 1 does the same job.
from collections import Counter
counts = Counter("banana")
print(counts) # Counter({'a': 3, 'n': 2, 'b': 1})
print(counts.most_common(1)) # [('a', 3)]
How do you solve two-sum in O(n)?
Walk the list once and remember each number's index in a dict. For each number, check whether its complement (target minus the number) was already seen.
def two_sum(nums, target):
seen = {}
for i, n in enumerate(nums):
if target - n in seen:
return [seen[target - n], i]
seen[n] = i
return []
print(two_sum([2, 7, 11, 15], 9)) # [0, 1]
How do you remove duplicates from a list but keep the order?
dict.fromkeys keeps the first occurrence of each item in order, because dicts preserve insertion order. set(items) also removes duplicates but loses the order.
items = [3, 1, 3, 2, 1]
print(list(dict.fromkeys(items))) # [3, 1, 2]
How do you flatten a nested list?
Use recursion for any depth: if an item is a list, flatten it, otherwise yield it. For exactly one level of nesting, a comprehension is enough.
def flatten(items):
for item in items:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
print(list(flatten([1, [2, [3, 4]], 5]))) # [1, 2, 3, 4, 5]
How do you find the second largest number in a list?
Keep the largest and second largest while scanning once, skipping repeats of the largest. This is O(n) and avoids sorting.
def second_largest(nums):
first = second = None
for n in nums:
if first is None or n > first:
first, second = n, first
elif n != first and (second is None or n > second):
second = n
return second
print(second_largest([4, 9, 9, 7])) # 7
How do you merge two dictionaries?
Since Python 3.9 use the | operator. {**a, **b} works in older versions. In both, values from the right-hand dict win when keys repeat.
a = {"x": 1, "y": 2}
b = {"y": 3}
print(a | b) # {'x': 1, 'y': 3}
print({**a, **b}) # same result
How do you generate Fibonacci numbers without storing them all?
Use a generator that keeps only the last two numbers. It can produce as many values as you ask for in constant memory.
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
from itertools import islice
print(list(islice(fibonacci(), 8))) # [0, 1, 1, 2, 3, 5, 8, 13]