Experienced Python Interview Questions
Why can’t a lambda function have statements in Python?
The body of a lambda must be one expression, because a lambda is only a short way to write a function that returns the value of that expression. Statements such as return, for, while, pass or an assignment are not expressions, so they are not allowed. Writing one gives a SyntaxError.
# This is NOT allowed
f = lambda x: return x * 2
You can still use an expression that makes a choice, like a conditional expression.
positive = lambda x: x if x > 0 else 0
print(positive(-5))
print(positive(8))
Output:
0
8
If you need many lines, loops or several statements, write a normal function with def. Also read What is a lambda function?
What are public, protected and private attributes in Python?
Python has no strict access control like Java or C++. Instead it uses naming rules to show how an attribute should be used.
| Name | Type | Meaning |
|---|---|---|
name | Public | Can be used from anywhere |
_name | Protected | By convention only for the class and its child classes. Python does not stop you |
__name | Private | Python renames it (name mangling), so it cannot be reached by its normal name from outside |
class Account:
def __init__(self):
self.owner = "Robin"
self._balance = 100
self.__pin = 1234
a = Account()
print(a.owner)
print(a._balance)
print(a.__pin)
Output:
Robin
100
AttributeError: 'Account' object has no attribute '__pin'
The private attribute is stored as _Account__pin behind the scenes, so it is hidden from casual use but not truly secret. See Python Access Modifiers for more.
What is a decorator in Python?
A decorator is a function that takes another function, adds some extra behaviour to it, and returns the new function. You apply it by writing @decorator_name above the function, and the original code stays unchanged.
def shout(func):
def wrapper():
result = func()
return result.upper()
return wrapper
@shout
def greet():
return "hello"
print(greet())
Output:
HELLO
Writing @shout above greet is the same as writing greet = shout(greet). Decorators are used for logging, timing, login checks and caching. Use functools.wraps inside real decorators to keep the original function name. Learn more in Python Decorators.
What are iterators in Python?
An iterator is an object that gives you the items of a collection one at a time. It follows the iterator protocol, which means it has two methods: __iter__() and __next__(). A list, tuple or string is an iterable, and you get an iterator from it with iter().
nums = [10, 20, 30]
it = iter(nums)
print(next(it))
print(next(it))
print(next(it))
print(next(it))
Output:
10
20
30
Traceback (most recent call last):
File "main.py", line 6, in <module>
print(next(it))
StopIteration
When there are no items left, the iterator raises StopIteration. A for loop calls iter() and next() for you and stops quietly when this happens. Compare it with a generator, which is an easy way to create an iterator.
What is a generator in Python?
A generator is a function that uses the yield keyword to give values one at a time, instead of building the whole list in memory. Each time it yields, the function pauses and remembers where it stopped, and it continues from the same place on the next request.
def count_up(n):
i = 1
while i <= n:
yield i
i += 1
for num in count_up(3):
print(num)
Output:
1
2
3
There is also a short form called a generator expression, which looks like a list comprehension but uses round brackets.
squares = (x * x for x in range(5))
print(sum(squares))
Output:
30
Generators are a good choice for big data, because values are made only when needed. A generator is also an iterator.
What is pickling and unpickling in Python?
Pickling is the process of converting a Python object into a stream of bytes so it can be saved in a file or sent over a network. Unpickling is the reverse: reading those bytes and rebuilding the original object. Both are done with the built-in pickle module.
import pickle
data = {"name": "Robin", "marks": [90, 85]}
with open("data.pkl", "wb") as f:
pickle.dump(data, f)
with open("data.pkl", "rb") as f:
loaded = pickle.load(f)
print(loaded)
Output:
{'name': 'Robin', 'marks': [90, 85]}
Files must be opened in binary mode (wb and rb). Important: never unpickle data from a source you do not trust, because it can run harmful code. For data that other programs or languages also need, JSON is the safer choice.
How does Python do compile-time and run-time checking?
Python checks only a small part at compile time, which is when the source is turned into bytecode. This step finds syntax errors, like a missing colon or wrong indentation. Most other checks, such as names and types, happen at run time, when that line actually runs.
def show():
print(undefined_name)
print("Program started")
show()
Output:
Program started
Traceback (most recent call last):
File "main.py", line 5, in <module>
show()
File "main.py", line 2, in show
print(undefined_name)
NameError: name 'undefined_name' is not defined
The code compiled without any problem, even though undefined_name does not exist. The error came only when show() was called. A syntax error is different: the program does not start at all. This is the cost of a dynamically typed language, so good tests are important.
What is PYTHONPATH?
PYTHONPATH is an environment variable that tells Python about extra folders to search when you import a module. Python normally looks in the current folder and the standard library first, and then in the folders listed in PYTHONPATH.
Set it on macOS or Linux like this:
export PYTHONPATH=/home/robin/mylibs
On Windows (Command Prompt):
set PYTHONPATH=C:\mylibs
You can check the full list of search folders from Python itself.
import sys
print(sys.path)
Output (your computer will show different folders):
[”, ‘/home/robin/mylibs’, ‘/usr/lib/python3.12’, …]
Your own folder appears in the list. This is useful when you keep shared code outside the project, but a virtual environment and proper packages are usually a cleaner way.
What is the difference between shallow copy and deep copy in Python?
A shallow copy creates a new outer object but the inner objects are still shared with the original. A deep copy creates a new outer object and also new copies of everything inside it.
import copy
original = [[1, 2], [3]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
original[0].append(99)
print(shallow)
print(deep)
Output:
[[1, 2, 99], [3]]
[[1, 2], [3]]
The shallow copy changed too, because its inner list is the same object as in the original. The deep copy stayed safe. Note that b = a does not copy at all, it only gives a second name to the same object. Use a deep copy when the data has lists or dictionaries inside other lists or dictionaries.
What is the GIL in Python?
The GIL (Global Interpreter Lock) is a lock inside CPython, the standard Python, that allows only one thread to run Python bytecode at a time. It keeps memory management simple and safe.
Because of the GIL, several threads doing heavy calculation do not run in parallel on different CPU cores. But it is released while a thread waits for files, network or database, so threads still work well for such waiting tasks.
| Task type | Better choice |
|---|---|
| Waiting for network, files, database (I/O bound) | threading or asyncio |
| Heavy calculation (CPU bound) | multiprocessing, where each process has its own interpreter and GIL |
Python 3.13 also added an experimental build that can run without the GIL, but the normal build still has it. Other implementations such as Jython do not have a GIL.
How does Python manage memory?
Python manages memory automatically. All objects live in a private heap, and Python frees them when they are no longer needed. It uses two ideas for this:
- Reference counting: every object keeps a count of how many names point to it. When the count becomes zero, the memory is freed at once.
- Garbage collector: finds groups of objects that point to each other (cycles) and are no longer used, which reference counting cannot free. It is available in the
gcmodule.
import sys
a = [1, 2, 3]
b = a
print(sys.getrefcount(a))
Output:
3
The count is 3 because of a, b and the temporary reference made while calling getrefcount(). You do not free memory yourself in Python, but you should avoid keeping big objects alive longer than needed.
What is the with statement in Python?
The with statement makes sure that setup and cleanup are done automatically, even if an error happens in between. It works with a context manager, which is an object that has __enter__() and __exit__() methods. The most common use is opening files.
with open("notes.txt", "w") as f:
f.write("Hello")
print(f.closed)
Output:
True
When the with block ends, the file is closed for you, so you do not need to call f.close(). It is also used for database connections, locks and temporary settings. See Python File Handling.