You say "lists are mutable, tuples are immutable" with total confidence. The interviewer nods and asks, "So can a tuple ever change?" You pause. You learnt the definition, not the reason, and the second question is where the reason gets tested.
That is the pattern of almost every Python round for freshers. The first question checks whether you have read a tutorial. The follow-up checks whether you have written code and watched it misbehave. This article covers 25 questions that come up again and again, with answers built to hold up when the interviewer pushes.
The short answer
Python interview questions for freshers, like most interview questions on Python programming, concentrate on a small set of ideas: which objects are mutable, how names refer to objects, how copying works, how iteration works under the hood (iterators, generators, comprehensions), functions as objects (decorators, *args, lambdas, scope), basic OOP, and a few runtime facts like the GIL and the cost of common operations. If you can explain each one in terms of what Python is actually doing with objects and references, most follow-ups answer themselves.
Data types and mutability
1. What is the difference between a list and a tuple?
A list is mutable: you can append, remove and reassign elements. A tuple is immutable: once created, its sequence of references cannot change. That immutability is why a tuple can be a dictionary key or a set member (if its contents are hashable) and why it signals "fixed record" to the reader, like a pair of coordinates.
Likely follow-up: "Can a tuple change?" The tuple itself cannot, but if it holds a mutable object, that object can. t = (1, [2, 3]) then t[1].append(4) works, because the tuple still points at the same list. Because it holds a list, t was never hashable in the first place, so it could not have been a dict key even before the append.
2. When would you use a set instead of a list?
When you care about membership or uniqueness, not order. A set is a hash table, so x in my_set is O(1) on average, while x in my_list scans every element, O(n). Converting a list to a set is also the quickest way to drop duplicates. The cost: elements must be hashable, and you lose positional indexing.
3. What can be used as a dictionary key?
Any hashable object: one whose hash never changes during its lifetime and that supports equality comparison. Strings, numbers and tuples of hashable items work. Lists, dicts and sets do not, because if a key's contents changed, its hash would change and the dictionary could no longer find it.
4. What is mutability, and how does it affect passing arguments to functions?
Python passes references to objects, sometimes described as "pass by object reference". If a function mutates a list it received, the caller sees the change, because both names point at one object. If the function rebinds the parameter (items = []), only the local name changes. Integers and strings appear to be "passed by value" only because they cannot be mutated at all.
5. Why is a mutable default argument a bug?
Default values are evaluated once, when the def statement runs, not on each call. So def add(item, bucket=[]) shares one list across every call that omits bucket, and items from earlier calls pile up. The fix is bucket=None and then if bucket is None: bucket = [] inside the function.
Likely follow-up: "Is it ever useful?" Occasionally, as a deliberate cache, but most teams would rather see functools.lru_cache because the intent is obvious.
6. What is the difference between is and ==?
== asks whether two objects have equal values, by calling __eq__. is asks whether two names point at the same object in memory. Use is for singletons such as None: write if x is None, not if x == None, because a class can override __eq__ but cannot fake identity.
Likely follow-up: "Why does a is b return True for small integers?" CPython caches integers from -5 to 256 and reuses them. That is an implementation detail, not a language guarantee, so never rely on is for numbers or strings.
7. What is the difference between a shallow copy and a deep copy?
A shallow copy (copy.copy(x), lst[:], list(lst)) creates a new outer container but fills it with references to the same inner objects. A deep copy (copy.deepcopy(x)) recursively copies the inner objects too. With a list of lists, mutating an inner list through a shallow copy changes the original as well. Deep copies are slower and copy everything, so use them only when nested data must be independent.
8. Strings are immutable. What does that mean in practice?
Any "modification", such as s.upper() or s.replace(), returns a new string and leaves the original untouched. That makes strings safe as dict keys and safe to share. The practical cost shows up in loops: building a long string with s += piece can create a new string each time, while collecting pieces in a list and calling "".join(pieces) once builds it in a single pass.
Functions
9. What are *args and **kwargs?
*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict. The names are convention; the stars do the work. They let you write wrappers and decorators that pass arguments through untouched: return func(*args, **kwargs). The same stars unpack at the call site, so f(*my_list) spreads a list into positional arguments.
10. What is a lambda, and how do map and filter relate to it?
A lambda is an anonymous function limited to a single expression, handy as a short key argument: sorted(users, key=lambda u: u.age). map applies a function to every item; filter keeps items where the function returns something truthy. In Python 3 both return lazy iterators, not lists, so you wrap them in list() to see results.
11. Explain Python's scope rules.
Name lookup follows LEGB: Local, then Enclosing function, then Global (module), then Built-in. Assigning to a name anywhere inside a function makes it local for the whole function, which is why reading count and then doing count += 1 raises UnboundLocalError. To rebind a module-level name, declare global; to rebind a variable from an enclosing function, declare nonlocal.
12. What is a decorator?
A function that takes a function and returns a replacement, usually a wrapper that adds behaviour before or after calling the original. @timer above def work() is just work = timer(work). That works because functions are objects you can pass around and close over.
Likely follow-up: "Why use functools.wraps?" Without it the wrapper replaces the original's __name__ and docstring, which confuses debugging tools and logs. @wraps(func) on the inner function copies them across.
Iteration
13. What is a list comprehension, and when should you avoid one?
[x * x for x in nums if x > 0] builds a list from a loop and an optional condition in one expression. It is usually clearer and slightly faster than an equivalent append loop. Avoid one when the logic needs several nested conditions, or when you only want side effects such as printing. In Python 3 the loop variable does not leak out of a comprehension.
14. What is the difference between an iterable and an iterator?
An iterable is anything you can loop over: it has __iter__, which returns an iterator. An iterator has __next__, which returns the next item or raises StopIteration when it is done, and an __iter__ that returns itself, so every iterator is also an iterable. A list is iterable but not an iterator, so you can loop over it repeatedly. An iterator is consumed once: loop over it a second time and you get nothing.
15. What is a generator, and what does yield do?
A generator function contains yield. Calling it does not run the body; it returns a generator object. Each next() runs the body until the next yield, hands back that value and freezes the function's local state until asked again. That makes generators lazy: reading a 10 GB log file line by line with a generator keeps one line in memory, not the whole file.
Likely follow-up: "List comprehension or generator expression?" Swap the square brackets for parentheses and you get a generator expression. Use it when you will iterate once, especially when passing straight into sum() or any(); use a list when you need indexing, length or multiple passes.
Errors and resources
16. How does try, except, else, finally work?
try holds the code that might fail. except handles specific exception types. else runs only if no exception was raised, which keeps the try block small, so you do not accidentally catch errors from code you never meant to guard. finally always runs, exception or not, and is for clean-up. Avoid a bare except:: it also swallows KeyboardInterrupt and hides real bugs.
17. What does the with statement do?
It uses a context manager: an object with __enter__ and __exit__. with open("data.txt") as f: guarantees the file is closed when the block ends, even if an exception is raised inside it. You can write your own with a class, or with contextlib.contextmanager and a generator that yields once.
Object-oriented Python
18. What are self and __init__?
self is the instance the method was called on. Python passes it explicitly: obj.greet() is roughly Class.greet(obj), which is why it must be the first parameter. __init__ is the initialiser that runs after the object is created, setting instance attributes. It is not strictly the constructor; __new__ creates the object, and you rarely need to touch it.
19. How does inheritance work, and what does super() do?
A subclass inherits attributes and methods from its parent and can override them. super().__init__(...) calls the next class's version in the method resolution order, so the parent can set up its own attributes instead of you copying that code. Forgetting it in a subclass __init__ is a common fresher bug: the parent's attributes never get created.
20. What is the MRO?
The method resolution order is the sequence Python searches for an attribute across a class hierarchy. With multiple inheritance, Python 3 computes it with the C3 linearisation algorithm, which keeps each class before its parents and preserves the order you listed the bases in. You can inspect it with ClassName.__mro__ or ClassName.mro(). This is also why super() means "next in the MRO", not "my parent".
21. What are dunder methods? Explain __str__ and __repr__.
Double-underscore methods let your objects work with built-in syntax: __len__ powers len(), __eq__ powers ==, __add__ powers +. __repr__ should be an unambiguous, developer-facing description, ideally something that could recreate the object; __str__ is the readable version for print(). If you define only __repr__, str() falls back to it.
Likely follow-up: "What happens if you define __eq__?" Python sets __hash__ to None, making instances unhashable, unless you also define __hash__ consistently with equality.
Runtime and tooling
22. What is the GIL, and should you use threads or processes?
The Global Interpreter Lock in CPython lets only one thread execute Python bytecode at a time. Threads still help with I/O-bound work, such as network calls, because a waiting thread releases the lock. For CPU-bound work, use multiprocessing or concurrent.futures.ProcessPoolExecutor: each process has its own interpreter and its own GIL, so work runs on several cores. Python 3.13 added an experimental free-threaded build without the GIL, and since 3.14 it is officially supported but still optional; the default build still has the GIL, so say which one you mean.
23. Are dictionaries ordered?
Yes, since Python 3.7 the language guarantees that dicts preserve insertion order. In CPython 3.6 it already happened as an implementation detail, and before that order was arbitrary. Updating an existing key keeps its position; deleting and reinserting moves it to the end.
24. What is the time complexity of common list and dict operations?
A list is a dynamic array: indexing and append are O(1) (append amortised), insert(0, x) and pop(0) are O(n) because every element shifts, and x in lst is O(n). Dict lookup, insertion and deletion are O(1) on average, degrading towards O(n) only with pathological hash collisions. If you need fast removal from the front, use collections.deque, where popleft() is O(1).
Likely follow-up: "How would you find duplicates in a list of a million items?" Walk it once with a set of seen values: O(n) time, rather than the O(n squared) of comparing every pair. For more on choosing structures by cost, read data structures you must know.
25. What is a virtual environment, and why use one?
A virtual environment is an isolated Python installation per project, created with python -m venv .venv. Packages installed with pip while it is active go into that environment, not the system Python, so project A can use one version of a library and project B another. Record dependencies with pip freeze > requirements.txt so someone else can recreate the environment with pip install -r requirements.txt.
How to use this list
Do not memorise these as paragraphs. For each question, open a Python shell and break the thing: trigger the mutable default bug, mutate a shallow copy, loop over an exhausted generator twice. Five minutes of watching it go wrong gives you the answer to the follow-up, which is the part most freshers miss.
Then practise saying it. Explaining why is and == differ while someone waits is a different skill from knowing it, and that is what an interviewer is grading. If a question lands that you genuinely cannot answer, how you reason through it still counts; see how to handle a question you don't know. If you are also preparing Java, the companion piece is Java interview questions for freshers, and the wider plan for your first round is on the fresher interview preparation page.
When you want to test yourself under real pressure, a Python practice round with Sia, our AI interviewer, asks one question at a time and follows up on what you actually said, at Beginner, Standard or Senior difficulty, with a written breakdown when you finish. For a human view, a live 40-minute session with a working engineer is scored on technical depth, problem solving, communication, behavioural and code quality, with written feedback on where your answers held up and where they did not.