Python Questions for Interview Preparation

#1
What is Python and what are its key features?

Key features:

  • Interpreted: code is executed line by line by the interpreter, without the need for compilation.
  • Dynamic typing: variable types are determined automatically at runtime, not at declaration.
  • Automatic memory management: a built-in Garbage Collector frees unused memory.
  • Cross-platform: code runs on Windows, macOS, and Linux without modifications.
  • Multi-paradigm: supports object-oriented, functional, and procedural programming styles.
  • Extensive standard library: modules for working with files, networking, JSON, and much more.

Where it's used:

  • Web development: Django, Flask, FastAPI.
  • Data analysis and ML: NumPy, Pandas, scikit-learn.
  • Automation and scripting: file processing, DevOps, testing.
View Answer
#2
What are the basic data types in Python?

Python provides several built-in data types:

Numeric types:

  • int — integers of arbitrary precision: 42, -7, 1_000_000
  • float — floating-point numbers: 3.14, -0.5, 1e10
  • complex — complex numbers: 3+4j

Text type:

  • str — character string: "Hello", 'Python'

Boolean type:

  • bool — takes values True or False

Special type:

  • NoneType — the single value None, meaning the absence of a value

Type conversion:

Python 3.13
# Explicit type conversion
x = int("42")       # str → int
y = float(42)       # int → float
z = str(3.14)       # float → str
w = bool(0)          # int → bool (False)

# Type checking
print(type(x))  # <class 'int'>
View Answer
#3
What is the difference between a list and a tuple?

List:

  • Mutable: you can add, remove, and modify elements.
  • Created using square brackets [].
  • Uses more memory due to resizing capability.
Python 3.13
fruits = ["apple", "banana", "cherry"]
fruits.append("pear")       # Add element
fruits[0] = "orange"        # Modify element

Tuple:

  • Immutable: cannot be changed after creation.
  • Created using parentheses ().
  • Faster and uses less memory.
  • Can be used as a dictionary key (since it's hashable).
Python 3.13
point = (10, 20)
# point[0] = 5  # TypeError — cannot modify

# Tuple as a dictionary key
locations = {(55.75, 37.62): "Moscow"}

When to use which:

  • list — when the collection will change (adding, removing elements).
  • tuple — when data should be immutable (coordinates, configurations, dictionary keys).
View Answer
#4
What is the difference between set and frozenset?

set:

  • A mutable unordered collection of unique elements.
  • Supports adding and removing elements.
  • Cannot be used as a dictionary key or element of another set.
Python 3.13
colors = {"red", "green", "blue"}
colors.add("yellow")
colors.discard("red")

frozenset:

  • An immutable version of a set.
  • Does not support adding or removing elements.
  • Is hashable — can be used as a dictionary key.
Python 3.13
immutable_set = frozenset([1, 2, 3])
# immutable_set.add(4)  # AttributeError

# frozenset as a dictionary key
cache = {frozenset([1, 2]): "result"}

Common set operations:

Python 3.13
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

a | b   # Union: {1, 2, 3, 4, 5, 6}
a & b   # Intersection: {3, 4}
a - b   # Difference: {1, 2}
a ^ b   # Symmetric difference: {1, 2, 5, 6}

In practice:

set is used for fast deduplication and membership checks (in runs in O(1)). frozenset is needed when a set must serve as a dictionary key or an element of another set.

View Answer
#5
What is a dictionary (dict) and how does it work?

Key characteristics:

  • Access, insertion, and deletion are performed in O(1) on average.
  • Keys must be hashable (strings, numbers, tuples).
  • Since Python 3.7, dictionaries preserve insertion order.
Python 3.13
user = {
    "name": "Anna",
    "age": 25,
    "city": "Moscow"
}

Main methods:

Python 3.13
user["name"]              # Access by key (KeyError if missing)
user.get("email", "—")    # Safe access with default value

user.keys()               # All keys
user.values()             # All values
user.items()              # Key-value pairs

user.pop("city")          # Remove and return value
user.update({"age": 26})  # Update values

Creating a dictionary:

Python 3.13
# Literal
d1 = {"a": 1, "b": 2}

# From a list of tuples
d2 = dict([("a", 1), ("b", 2)])

# Using dict comprehension
d3 = {x: x ** 2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
View Answer
#6
What are mutable and immutable data types?

Immutable:

  • int, float, bool
  • str
  • tuple
  • frozenset

When "modified," a new object is created:

Python 3.13
x = 10
print(id(x))  # e.g.: 140234866357520
x += 1
print(id(x))  # Different id — this is a new object

Mutable:

  • list
  • dict
  • set

The object is modified "in place":

Python 3.13
lst = [1, 2, 3]
print(id(lst))  # e.g.: 140234866400064
lst.append(4)
print(id(lst))  # Same id — object was modified

Why this matters:

  • Dictionary keys can only be immutable objects.
  • Passing to functions: mutable objects can be changed inside a function, which may cause unexpected side effects.
  • Default values: don't use mutable objects as default values in functions.
Python 3.13
# Common mistake
def add_item(item, lst=[]):  # Same list across all calls!
    lst.append(item)
    return lst

# Correct approach
def add_item(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst
View Answer
#7
How do indexing and slicing work in Python?

Indexing:

  • Elements are numbered starting from 0.
  • Negative indices count from the end: -1 is the last element.
Python 3.13
text = "Python"
text[0]    # 'P'
text[-1]   # 'n'
text[-2]   # 'o'

Slicing:

Syntax: [start:stop:step]

  • start — starting index (inclusive), defaults to 0.
  • stop — ending index (exclusive), defaults to the length of the sequence.
  • step — step size, defaults to 1.
Python 3.13
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

nums[2:5]     # [2, 3, 4]
nums[:3]      # [0, 1, 2]
nums[7:]      # [7, 8, 9]
nums[::2]     # [0, 2, 4, 6, 8]  — every second
nums[::-1]    # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]  — reverse

Slicing works with strings and tuples:

Python 3.13
text = "Hello, World!"
text[7:12]    # 'World'
text[::-1]    # '!dlroW ,olleH'

coords = (10, 20, 30, 40, 50)
coords[1:4]   # (20, 30, 40)
View Answer
#8
What are f-strings and how do they compare to other formatting methods?

Python offers several ways to format strings:

f-strings — recommended approach:

Available since Python 3.6. Allow embedding expressions directly in the string.

Python 3.13
name = "Anna"
age = 25
print(f"Hello, {name}! You are {age} years old.")
print(f"In 5 years you will be {age + 5}.")
print(f"Name in uppercase: {name.upper()}")

.format() method:

Python 3.13
print("Hello, {}! You are {} years old.".format(name, age))
print("Hello, {0}! {0}, you are {1} years old.".format(name, age))

% operator (legacy):

Python 3.13
print("Hello, %s! You are %d years old." % (name, age))

Comparison:

  • f-strings — the most readable and fastest method. Supports any expressions.
  • .format() — useful when the string template is defined in advance.
  • % — legacy approach, found in older code.

Number formatting:

Python 3.13
pi = 3.14159265
print(f"Pi: {pi:.2f}")          # Pi: 3.14
print(f"Number: {1000000:,}")   # Number: 1,000,000
print(f"Percent: {0.856:.1%}")  # Percent: 85.6%
View Answer
#9
How do if / elif / else conditional statements work?

Conditional statements allow you to execute different blocks of code depending on a condition.

Syntax:

Python 3.13
age = 18

if age < 13:
    print("Child")
elif age < 18:
    print("Teenager")
else:
    print("Adult")

Ternary operator:

A concise way to write a condition in a single line:

Python 3.13
status = "adult" if age >= 18 else "minor"

Truthy and Falsy values:

In Python, the following values are considered false (Falsy):

  • False, None
  • 0, 0.0
  • Empty collections: "", [], (), {}, set()

Everything else is considered true (Truthy):

Python 3.13
items = []

if items:
    print("List is not empty")
else:
    print("List is empty")  # This block will execute

Chained comparisons:

Python supports chained comparisons:

Python 3.13
x = 5
if 1 < x < 10:
    print("x is in the range from 1 to 10")
View Answer
#10
What kind of loops exist in Python and how do they differ?

for loop:

Iterates over the elements of an iterable object (list, string, range, etc.):

Python 3.13
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)

# Loop over a range of numbers
for i in range(5):
    print(i)  # 0, 1, 2, 3, 4

while loop:

Executes as long as the condition is true:

Python 3.13
count = 0
while count < 3:
    print(count)
    count += 1

Control statements:

  • break — terminates the loop.
  • continue — skips to the next iteration.
Python 3.13
for i in range(10):
    if i == 3:
        continue  # Skips 3
    if i == 7:
        break     # Stops the loop at 7
    print(i)      # 0, 1, 2, 4, 5, 6

else block in a loop:

Executes if the loop completes without encountering a break:

Python 3.13
for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            break
    else:
        # Executes if break was not triggered
        print(f"{n} is a prime number")
View Answer
#11
What is a function in Python and how do you define it?

Definition and calling:

Python 3.13
def greet(name):
    return f"Hello, {name}!"

message = greet("Anna")
print(message)  # Hello, Anna!

Parameters and arguments:

Python 3.13
# Default values
def power(base, exponent=2):
    return base ** exponent

power(3)     # 9  (exponent = 2)
power(3, 3)  # 27 (exponent = 3)

Keyword arguments:

Python 3.13
def create_user(name, age, city="Moscow"):
    return {"name": name, "age": age, "city": city}

# Keyword arguments can be passed in any order
user = create_user(age=25, name="Anna")

Returning multiple values:

Python 3.13
def min_max(numbers):
    return min(numbers), max(numbers)

lo, hi = min_max([3, 1, 7, 2, 9])
print(lo, hi)  # 1 9

Function without return:

If return is absent, the function returns None:

Python 3.13
def say_hello(name):
    print(f"Hello, {name}!")

result = say_hello("World")
print(result)  # None
View Answer
#12
What are *args and **kwargs?

*args — arbitrary number of positional arguments:

Collects all extra positional arguments into a tuple:

Python 3.13
def total(*args):
    return sum(args)

total(1, 2, 3)      # 6
total(10, 20)        # 30

**kwargs — arbitrary number of keyword arguments:

Collects all extra keyword arguments into a dictionary:

Python 3.13
def build_profile(**kwargs):
    return kwargs

build_profile(name="Anna", age=25, city="Moscow")
# {'name': 'Anna', 'age': 25, 'city': 'Moscow'}

Combining:

The order of parameters in a function definition is strictly fixed: regular → *args → keyword-only → **kwargs:

Python 3.13
def func(a, b, *args, **kwargs):
    print(f"a={a}, b={b}")
    print(f"args={args}")
    print(f"kwargs={kwargs}")

func(1, 2, 3, 4, x=10, y=20)
# a=1, b=2
# args=(3, 4)
# kwargs={'x': 10, 'y': 20}

Unpacking during a call:

Python 3.13
def greet(name, age):
    print(f"{name}, {age} years old")

args_list = ["Anna", 25]
greet(*args_list)      # Unpacking a list

kwargs_dict = {"name": "Ivan", "age": 30}
greet(**kwargs_dict)   # Unpacking a dictionary
View Answer
#13
What is an anonymous function (lambda)?

Syntax:

Python 3.13
# Regular function
def square(x):
    return x ** 2

# Equivalent lambda
square = lambda x: x ** 2

square(5)  # 25

Usage with sorted:

Python 3.13
users = [
    {"name": "Anna", "age": 25},
    {"name": "Boris", "age": 30},
    {"name": "Vera", "age": 20},
]

# Sort by age
sorted_users = sorted(users, key=lambda u: u["age"])

Usage with map and filter:

Python 3.13
numbers = [1, 2, 3, 4, 5]

squares = list(map(lambda x: x ** 2, numbers))
# [1, 4, 9, 16, 25]

evens = list(filter(lambda x: x % 2 == 0, numbers))
# [2, 4]

Limitations:

  • Only one expression — you cannot use multi-line logic, loops, or assignments.
  • Reduces readability for complex expressions — it's better to use a regular function.
  • No name — makes debugging harder (appears as <lambda> in the traceback).
View Answer
#14
What are variable scopes in Python?

The LEGB Rule:

  • L — Local: variables inside the current function.
  • E — Enclosing: variables in the outer (enclosing) function.
  • G — Global: variables at the module level.
  • B — Built-in: Python's built-in names (print, len, range).
Python 3.13
x = "global"  # Global

def outer():
    x = "enclosing"  # Enclosing

    def inner():
        x = "local"  # Local
        print(x)  # "local"

    inner()

outer()

The global keyword:

Allows modifying a global variable inside a function:

Python 3.13
counter = 0

def increment():
    global counter
    counter += 1

increment()
print(counter)  # 1

The nonlocal keyword:

Allows modifying a variable from an outer (enclosing) function:

Python 3.13
def outer():
    count = 0

    def inner():
        nonlocal count
        count += 1
        return count

    return inner

counter = outer()
print(counter())  # 1
print(counter())  # 2
View Answer
#15
What is list comprehension and how to use it?

Syntax:

Python 3.13
[expression for item in iterable]

Examples:

Python 3.13
# Squares of numbers
squares = [x ** 2 for x in range(6)]
# [0, 1, 4, 9, 16, 25]

# Equivalent with a loop
squares = []
for x in range(6):
    squares.append(x ** 2)

With condition (filtering):

Python 3.13
# Only even numbers
evens = [x for x in range(10) if x % 2 == 0]
# [0, 2, 4, 6, 8]

With if/else condition (transformation):

Python 3.13
labels = ["even" if x % 2 == 0 else "odd" for x in range(5)]
# ['even', 'odd', 'even', 'odd', 'even']

Nested comprehensions:

Python 3.13
# Multiplication table
matrix = [[i * j for j in range(1, 4)] for i in range(1, 4)]
# [[1, 2, 3], [2, 4, 6], [3, 6, 9]]

Analogs for other types:

Python 3.13
# Dict comprehension
squares_dict = {x: x ** 2 for x in range(5)}

# Set comprehension
unique_lengths = {len(word) for word in ["cat", "dog", "fox"]}

Important:

Avoid overusing complex nested comprehensions — if the expression is hard to read, use a regular loop instead.

View Answer
#16
What are generators and how do they differ from lists?

Generator function:

Python 3.13
def count_up_to(n):
    i = 1
    while i <= n:
        yield i
        i += 1

for num in count_up_to(5):
    print(num)  # 1, 2, 3, 4, 5

Generator expression:

Similar to a list comprehension, but uses parentheses:

Python 3.13
# List comprehension — creates the whole list in memory
squares_list = [x ** 2 for x in range(1000000)]

# Generator expression — evaluates one by one
squares_gen = (x ** 2 for x in range(1000000))

Key differences from a list:

  • Memory: a generator only stores the current item, not the entire collection.
  • Single use: a generator can be iterated over only once.
  • Laziness: items are evaluated on demand, not in advance.
Python 3.13
gen = (x for x in range(3))
print(list(gen))  # [0, 1, 2]
print(list(gen))  # [] — already exhausted

When to use:

  • Generator — when working with large datasets, streams, or when there is no need to keep all items.
  • List — when multiple access, indexing is required, or the dataset is known to be small.
View Answer
#17
How do the built-in map, filter, and zip functions work?

map — applies a function to each item:

Python 3.13
numbers = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x ** 2, numbers))
# [1, 4, 9, 16, 25]

# Equivalent using comprehension
squares = [x ** 2 for x in numbers]

filter — selects items by condition:

Python 3.13
numbers = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda x: x % 2 == 0, numbers))
# [2, 4, 6]

# Equivalent using comprehension
evens = [x for x in numbers if x % 2 == 0]

zip — combines multiple collections:

Python 3.13
names = ["Anna", "Boris", "Vera"]
ages = [25, 30, 22]

pairs = list(zip(names, ages))
# [('Anna', 25), ('Boris', 30), ('Vera', 22)]

# Often used to create a dictionary
user_ages = dict(zip(names, ages))
# {'Anna': 25, 'Boris': 30, 'Vera': 22}

Features:

  • All three functions return iterators, not lists — you need to wrap them in list() to get a list.
  • zip stops according to the shortest collection.
  • In most cases, list comprehension is considered a more readable alternative to map and filter.
View Answer
#18
What is unpacking in Python?

Multiple assignment:

Python 3.13
a, b, c = [1, 2, 3]
print(a, b, c)  # 1 2 3

# Works with tuples, strings, and other iterables
x, y = (10, 20)
first, second, third = "abc"

Variable swapping (swap):

Python 3.13
a, b = 1, 2
a, b = b, a
print(a, b)  # 2 1

Unpacking with * (asterisk):

Collects the "remaining" items into a list:

Python 3.13
first, *rest = [1, 2, 3, 4, 5]
print(first)  # 1
print(rest)   # [2, 3, 4, 5]

first, *middle, last = [1, 2, 3, 4, 5]
print(middle)  # [2, 3, 4]

Unpacking in function calls:

Python 3.13
def greet(name, age, city):
    print(f"{name}, {age}, {city}")

data = ["Anna", 25, "Moscow"]
greet(*data)  # Unpacking a list

info = {"name": "Ivan", "age": 30, "city": "St. Petersburg"}
greet(**info)  # Unpacking a dictionary

Unpacking in nested structures:

Python 3.13
points = [(1, 2), (3, 4), (5, 6)]
for x, y in points:
    print(f"x={x}, y={y}")
View Answer
#19
How does exception handling work in Python?

try/except syntax:

Python 3.13
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Division by zero!")

Full try/except/else/finally construct:

Python 3.13
try:
    number = int(input("Enter a number: "))
except ValueError:
    print("That's not a number!")
else:
    # Executes if NO exception occurred
    print(f"You entered: {number}")
finally:
    # ALWAYS executes
    print("Shutting down")

Catching multiple exceptions:

Python 3.13
try:
    value = int("abc")
except (ValueError, TypeError) as e:
    print(f"Error: {e}")

Hierarchy of main exceptions:

  • BaseException
    • Exception — base class for most exceptions
      • ValueError — invalid value
      • TypeError — invalid type
      • KeyError — key not found in dictionary
      • IndexError — index out of range for a list
      • FileNotFoundError — file not found
      • ZeroDivisionError — division by zero

Important rule:

Catch specific exceptions instead of a generic except Exception — this helps avoid hiding unexpected errors.

View Answer
#20
How to create a custom exception?

Simple custom exception:

Python 3.13
class InsufficientFundsError(Exception):
    pass

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError("Insufficient funds")
    return balance - amount

try:
    withdraw(100, 200)
except InsufficientFundsError as e:
    print(e)  # Insufficient funds

Exception with additional data:

Python 3.13
class ValidationError(Exception):
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

try:
    raise ValidationError("email", "Invalid format")
except ValidationError as e:
    print(e.field)    # email
    print(e.message)  # Invalid format

When to create custom exceptions:

  • When built-in exceptions don't describe the error accurately enough.
  • To separate business logic errors (e.g., UserNotFoundError, PermissionDeniedError).
  • To easily catch a group of related errors using a common base class.
Python 3.13
class AppError(Exception):
    """Base application exception"""
    pass

class NotFoundError(AppError):
    pass

class AccessDeniedError(AppError):
    pass
View Answer
#21
What is a context manager and how does the with statement work?

The with statement:

The most common example is working with files:

Python 3.13
# Without with — you must remember to close the file
file = open("data.txt", "r")
try:
    content = file.read()
finally:
    file.close()

# With with — the file gets closed automatically
with open("data.txt", "r") as file:
    content = file.read()
# The file is already closed

How it works:

  • __enter__() — called upon entering the with block. Its return value is assigned to the variable after as.
  • __exit__() — called upon exiting the with block, even if an exception was raised.

Creating a custom context manager:

Python 3.13
class Timer:
    def __enter__(self):
        import time
        self.start = time.time()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        elapsed = time.time() - self.start
        print(f"Execution time: {elapsed:.2f} sec")
        return False  # Don't suppress exceptions

with Timer():
    total = sum(range(1_000_000))
# Execution time: 0.03 sec

Where context managers are used:

  • Working with files (open)
  • Locks in multithreading (threading.Lock)
  • Database connections (automatic commit/rollback)
  • Temporary resources (temporary files, network connections)
View Answer
#22
How to read and write files in Python?

Opening a file:

The open() function takes the path to the file and a mode:

  • 'r' — read (default)
  • 'w' — write (overwrites the file)
  • 'a' — append (adds to the end of the file)
  • 'rb' / 'wb' — read / write in binary mode

Reading a file:

Python 3.13
# Read the entire file
with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()

# Read line by line
with open("data.txt", "r", encoding="utf-8") as f:
    for line in f:
        print(line.strip())

# Read all lines into a list
with open("data.txt", "r", encoding="utf-8") as f:
    lines = f.readlines()

Writing to a file:

Python 3.13
# Overwrite the file
with open("output.txt", "w", encoding="utf-8") as f:
    f.write("First line\n")
    f.write("Second line\n")

# Append to the end
with open("output.txt", "a", encoding="utf-8") as f:
    f.write("Another line\n")

Encodings:

Always specify encoding="utf-8" to avoid issues with special characters and non-Latin alphabets:

Python 3.13
# Without specifying encoding, a UnicodeDecodeError might occur
with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()

Important:

Always use the with statement — it ensures the file is properly closed even if an error occurs.

View Answer
#23
What are classes and objects in Python?

Defining a class:

Python 3.13
class Dog:
    # Class attribute (shared by all instances)
    species = "Canis familiaris"

    # Constructor — gets called when creating an object
    def __init__(self, name, age):
        # Instance attributes (unique to each object)
        self.name = name
        self.age = age

    # Instance method
    def bark(self):
        return f"{self.name} says: Woof!"

Creating objects:

Python 3.13
dog1 = Dog("Bobik", 3)
dog2 = Dog("Sharik", 5)

print(dog1.name)      # Bobik
print(dog2.bark())    # Sharik says: Woof!
print(dog1.species)   # Canis familiaris

self:

  • self is a reference to the current instance of the class.
  • It is passed automatically when a method is called.
  • Through self, you can access the object's attributes and methods.

Class attributes vs instance attributes:

Python 3.13
class Counter:
    count = 0  # Class attribute — shared by all

    def __init__(self):
        Counter.count += 1  # Modify the class attribute
        self.id = Counter.count  # Instance attribute

c1 = Counter()
c2 = Counter()
print(Counter.count)  # 2
print(c1.id, c2.id)   # 1 2
View Answer
#24
What is inheritance and how does it work in Python?

Basic inheritance:

Python 3.13
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} makes a sound"

class Dog(Animal):
    def speak(self):
        return f"{self.name} says: Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says: Meow!"

dog = Dog("Bobik")
print(dog.speak())  # Bobik says: Woof!

super() — calling a parent method:

Python 3.13
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)  # Call parent's __init__
        self.breed = breed

dog = Dog("Bobik", 3, "Labrador")
print(dog.name, dog.breed)  # Bobik Labrador

Checking inheritance:

Python 3.13
print(isinstance(dog, Dog))     # True
print(isinstance(dog, Animal))  # True
print(issubclass(Dog, Animal))  # True

Overriding methods:

A child class can replace or extend a parent's method:

Python 3.13
class Shape:
    def area(self):
        return 0

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

rect = Rectangle(5, 3)
print(rect.area())  # 15

Multiple inheritance:

Python supports inheriting from multiple classes. The method lookup order is determined by the MRO (Method Resolution Order) algorithm, which can be inspected via ClassName.mro().

View Answer
#25
What is encapsulation in Python?

Naming conventions:

In Python, there are no strict access modifiers (private, public). Instead, conventions are used:

  • name — public attribute, accessible to everyone.
  • _name — "protected" (by convention), intended for internal use only.
  • __name — "private", Python applies name mangling (changing the name).
Python 3.13
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # "Private" attribute

    def get_balance(self):
        return self.__balance

account = BankAccount(1000)
# print(account.__balance)  # AttributeError
print(account.get_balance())  # 1000

# Name mangling — the attribute is accessible via the modified name
print(account._BankAccount__balance)  # 1000

@property — controlled access:

Allows using methods as if they were attributes:

Python 3.13
class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Below absolute zero!")
        self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9 / 5 + 32

temp = Temperature(25)
print(temp.celsius)     # 25
print(temp.fahrenheit)  # 77.0
temp.celsius = 30       # Uses the setter
View Answer
#26
What is polymorphism in Python?

Polymorphism through method overriding:

Python 3.13
class Cat:
    def speak(self):
        return "Meow!"

class Dog:
    def speak(self):
        return "Woof!"

class Duck:
    def speak(self):
        return "Quack!"

# Same interface, different behavior
animals = [Cat(), Dog(), Duck()]
for animal in animals:
    print(animal.speak())

Duck Typing:

Python does not check the object's type — what matters is the presence of the required method:

Python 3.13
class File:
    def read(self):
        return "data from a file"

class Database:
    def read(self):
        return "data from a DB"

def load_data(source):
    # The type doesn't matter, only that it has a 'read' method
    return source.read()

print(load_data(File()))      # data from a file
print(load_data(Database()))  # data from a DB

Polymorphism of built-in functions:

Python 3.13
# len() works with different types
print(len("Python"))    # 6
print(len([1, 2, 3]))   # 3
print(len({"a": 1}))    # 1

# + behaves differently
print(1 + 2)           # 3 (addition)
print("Hello, " + "world!")  # Hello, world! (concatenation)
View Answer
#27
What are static and class methods?

Regular method (instance method):

Receives a reference to the instance (self) as its first argument:

Python 3.13
class MyClass:
    def instance_method(self):
        return f"Called for {self}"

@classmethod — a method of the class:

Receives a reference to the class (cls) instead of an instance:

Python 3.13
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def from_string(cls, data_string):
        name, age = data_string.split(",")
        return cls(name, int(age))

# Alternative constructor
user = User.from_string("Anna,25")
print(user.name)  # Anna

@staticmethod — a static method:

Receives neither self nor cls. It behaves like a regular function placed inside a class namespace:

Python 3.13
class MathUtils:
    @staticmethod
    def is_even(n):
        return n % 2 == 0

print(MathUtils.is_even(4))  # True

When to use which:

  • Instance method — when you need access to the object's attributes (self).
  • @classmethod — for alternative constructors or when modifying class attributes.
  • @staticmethod — for utility functions that logically belong to the class but do not need access to the instance or the class.
View Answer
#28
What are magic (dunder) methods?

String representation:

Python 3.13
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __str__(self):
        # For the end user (print, str())
        return f"{self.name}: ${self.price}"

    def __repr__(self):
        # For the developer (debugging, repr())
        return f"Product('{self.name}', {self.price})"

p = Product("Book", 50)
print(p)       # Book: $50
print(repr(p)) # Product('Book', 50)

Object comparison:

Python 3.13
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __lt__(self, other):
        return (self.x ** 2 + self.y ** 2) < (other.x ** 2 + other.y ** 2)

Point(1, 2) == Point(1, 2)  # True
Point(1, 2) < Point(3, 4)   # True

Other useful magic methods:

  • __len__ — behavior for len(obj)
  • __getitem__ — index access obj[key]
  • __contains__ — the in operator
  • __add__ — the + operator
  • __call__ — calling an object like a function obj()
Python 3.13
class Basket:
    def __init__(self):
        self.items = []

    def __len__(self):
        return len(self.items)

    def __contains__(self, item):
        return item in self.items

basket = Basket()
basket.items.append("apple")
print(len(basket))           # 1
print("apple" in basket)     # True
View Answer
#29
What are decorators and how do they work?

A simple decorator:

Python 3.13
def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Result: {result}")
        return result
    return wrapper

@log_call
def add(a, b):
    return a + b

add(3, 5)
# Calling function: add
# Result: 8

How it works under the hood:

Python 3.13
# Using @decorator syntax
@log_call
def add(a, b):
    return a + b

# Is equivalent to:
def add(a, b):
    return a + b
add = log_call(add)

Preserving metadata (@wraps):

Python 3.13
from functools import wraps

def log_call(func):
    @wraps(func)  # Keeps the original name and docstring
    def wrapper(*args, **kwargs):
        print(f"Calling: {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_call
def add(a, b):
    """Adds two numbers."""
    return a + b

print(add.__name__)  # add (without @wraps it would be 'wrapper')
print(add.__doc__)   # Adds two numbers.

Practical examples of decorators:

  • Logging function calls
  • Measuring execution time
  • Caching results
  • Access control/authentication
  • Input validation
View Answer
#30
What are abstract classes and why are they needed?

The abc module:

Python 3.13
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        """Calculate the area of the shape"""
        pass

    @abstractmethod
    def perimeter(self):
        """Calculate the perimeter of the shape"""
        pass

# shape = Shape()  # TypeError: Can't instantiate abstract class Create

Implementing an abstract class:

Python 3.13
class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        import math
        return math.pi * self.radius ** 2

    def perimeter(self):
        import math
        return 2 * math.pi * self.radius

rect = Rectangle(5, 3)
print(rect.area())       # 15
print(rect.perimeter())  # 16

Why they are needed:

  • Contract: they guarantee that any child class implements all the required methods.
  • Documentation: they clearly show what methods must be implemented.
  • Early error detection: if an abstract method is missing in the child class, Python will raise a TypeError at instantiation, rather than failing later when the method is called.
View Answer
#31
How does importing modules work in Python?

Ways to import:

Python 3.13
# Importing an entire module
import math
print(math.sqrt(16))  # 4.0

# Importing specific objects
from math import sqrt, pi
print(sqrt(16))  # 4.0

# Importing with an alias
import datetime as dt
now = dt.datetime.now()

Creating your own module:

Python 3.13
# utils.py
def greet(name):
    return f"Hello, {name}!"

# main.py
from utils import greet
print(greet("World"))

__name__ == "__main__":

Allows distinguishing whether the file is run directly or imported as a module:

Python 3.13
# my_module.py
def main():
    print("Running the module directly")

if __name__ == "__main__":
    # This code executes only when running directly:
    # python my_module.py
    main()

Packages:

A package is a directory with an __init__.py file containing multiple modules:

my_package/
    __init__.py
    module_a.py
    module_b.py
Python 3.13
from my_package.module_a import some_function
View Answer
#32
What is a virtual environment and why is it needed?

Why it's needed:

  • Different projects might require different versions of the same library.
  • To avoid conflicts between dependencies of different projects.
  • To lock exact package versions for reproducibility.

Creation and usage (venv):

# Create a virtual environment
python -m venv venv

# Activation
# macOS/Linux:
source venv/bin/activate
# Windows:
venv\Scripts\activate

# Deactivation
deactivate

Package management (pip):

# Install a package
pip install requests

# Install a specific version
pip install requests==2.31.0

# List installed packages
pip list

# Save dependencies to a file
pip freeze > requirements.txt

# Install dependencies from a file
pip install -r requirements.txt

requirements.txt:

requests==2.31.0
flask==3.0.0
pytest==7.4.3

This file locks all project dependencies, allowing you to recreate the environment on another machine.

View Answer
#33
How to work with JSON in Python?

Main functions:

  • json.dumps() — Python object → JSON string
  • json.loads() — JSON string → Python object
  • json.dump() — Python object → JSON file
  • json.load() — JSON file → Python object

Serialization (Python → JSON):

Python 3.13
import json

data = {
    "name": "Anna",
    "age": 25,
    "hobbies": ["reading", "swimming"],
    "active": True
}

# To a string
json_string = json.dumps(data, ensure_ascii=False, indent=2)
print(json_string)

# To a file
with open("data.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

Deserialization (JSON → Python):

Python 3.13
# From a string
json_string = '{"name": "Anna", "age": 25}'
data = json.loads(json_string)
print(data["name"])  # Anna

# From a file
with open("data.json", "r", encoding="utf-8") as f:
    data = json.load(f)

Type mapping:

JSONPython
objectdict
arraylist
stringstr
numberint/float
true/falseTrue/False
nullNone
View Answer
#34
What are type annotations in Python?

Basic syntax:

Python 3.13
def greet(name: str) -> str:
    return f"Hello, {name}!"

age: int = 25
price: float = 19.99
is_active: bool = True

Collections:

Python 3.13
# Python 3.9+
def process(items: list[str]) -> dict[str, int]:
    return {item: len(item) for item in items}

# Nested types
matrix: list[list[int]] = [[1, 2], [3, 4]]

The typing module:

Python 3.13
from typing import Optional, Union

# Can be str or None
def find_user(user_id: int) -> Optional[str]:
    if user_id == 1:
        return "Anna"
    return None

# Can be int or str
def parse(value: Union[int, str]) -> str:
    return str(value)

Why use annotations:

  • Documentation: the expected types are clear without reading the function body.
  • IDEs: autocomplete and code suggestions work better.
  • Static analysis: tools like mypy can catch type errors prior to running code.
  • Collaboration: makes the codebase easier for other developers to understand.

Important note:

Annotations are merely hints, not strict enforcements. Python does not enforce types at runtime:

Python 3.13
def add(a: int, b: int) -> int:
    return a + b

add("hello", " world")  # Will run without error yielding: "hello world"
View Answer
#35
What is the difference between a shallow copy and a deep copy?

Shallow copy:

Creates a new object but does not copy nested objects — it maintains references to the originals:

Python 3.13
import copy

original = [[1, 2, 3], [4, 5, 6]]
shallow = copy.copy(original)

shallow[0][0] = 999
print(original[0][0])  # 999 — the original has changed too!

Deep copy:

Creates a completely independent clone, including all nested objects:

Python 3.13
import copy

original = [[1, 2, 3], [4, 5, 6]]
deep = copy.deepcopy(original)

deep[0][0] = 999
print(original[0][0])  # 1 — the original remains unchanged

Ways to make a shallow copy:

Python 3.13
# For lists
lst = [1, 2, 3]
copy1 = lst.copy()
copy2 = lst[:]
copy3 = list(lst)

# For dictionaries
d = {"a": 1}
copy4 = d.copy()
copy5 = dict(d)

# Universal
import copy
copy6 = copy.copy(lst)

When to use which:

  • Shallow — when the collection only contains immutable elements (numbers, strings).
  • Deep — when there are nested mutable objects (lists within lists, dictionaries within lists).
Python 3.13
# Shallow is sufficient
nums = [1, 2, 3]  # Elements are immutable ints
safe_copy = nums.copy()

# Deep copy is required
matrix = [[1, 2], [3, 4]]  # Nested lists
safe_copy = copy.deepcopy(matrix)
View Answer
#36
What are iterators and the iteration protocol?

The iteration protocol:

  • __iter__() — returns the iterator object itself.
  • __next__() — returns the next element or raises a StopIteration exception.

How a for loop works:

Python 3.13
# What 'for' does under the hood
nums = [1, 2, 3]

# for num in nums:
#     print(num)

# Equivalent:
iterator = iter(nums)       # Calls nums.__iter__()
while True:
    try:
        num = next(iterator)  # Calls iterator.__next__()
        print(num)
    except StopIteration:
        break

Creating a custom iterator:

Python 3.13
class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value

for num in Countdown(5):
    print(num)  # 5, 4, 3, 2, 1

Built-in iterables:

  • list, tuple, str, dict, set are iterable (they have an __iter__ method).
  • range(), map(), filter(), zip() return iterators.
  • An iterator can only be traversed once, while an iterable object can be looped over multiple times.

In practice:

The iteration protocol underpins for loops, generators, and many built-in functions. Custom iterators are useful when you need to process data in chunks — for example, reading a large file line by line instead of loading it all into memory.

View Answer
#37
What is the difference between is and ==?

== (value comparison):

Checks whether the values match between two objects:

Python 3.13
a = [1, 2, 3]
b = [1, 2, 3]

print(a == b)  # True — values are the same

is (identity comparison):

Checks whether two variables point to the exact same object in memory:

Python 3.13
a = [1, 2, 3]
b = [1, 2, 3]

print(a is b)  # False — they are different objects
print(id(a), id(b))  # Different memory addresses

c = a
print(a is c)  # True — c points to the same object

Caching of small integers:

Python caches integers from -5 to 256, so:

Python 3.13
x = 100
y = 100
print(x is y)  # True — cached object

x = 1000
y = 1000
print(x is y)  # False — different objects

Rules of usage:

  • Use is only for comparison against None:
Python 3.13
# Correct
if value is None:
    print("No value")

# Incorrect
if value == None:
    print("No value")
  • When comparing values, always use ==.
  • is only checks the id() of the objects — this is useful mostly for singletons like None, True, or False.
View Answer
#38
How does memory management work in Python?

Reference Counting:

Every object keeps track of its reference count — the number of variables pointing to it. When the count reaches zero, the object is deleted:

Python 3.13
import sys

a = [1, 2, 3]
print(sys.getrefcount(a))  # 2 (a + the function parameter itself)

b = a  # Another reference
print(sys.getrefcount(a))  # 3

del b  # Remove a reference
print(sys.getrefcount(a))  # 2

Garbage Collector:

Reference counting fails when there are circular references:

Python 3.13
# Circular reference
a = []
b = []
a.append(b)
b.append(a)
del a, b
# The reference count won't reach zero, but the objects are unreachable

To solve this, Python runs a Garbage Collector (the gc module) that discovers and cleans up such cycles.

Key takeaways:

  • Reference counting is the primary mechanism and runs immediately.
  • Garbage collector is a secondary mechanism designed to break circular references.
  • del removes a reference to an object, not necessarily the object itself.
  • Developers rarely need to get involved in memory management — Python handles it perfectly.
View Answer
#39
What is a closure in Python?

How it works:

Python 3.13
def make_multiplier(factor):
    def multiply(number):
        return number * factor  # 'factor' is captured from the outer function
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15

Conditions for a closure:

  • There must be a nested function.
  • The nested function must refer to an environment variable from the enclosing function.
  • The enclosing function must return the nested function.

Practical applications:

Python 3.13
# Counter
def make_counter(start=0):
    count = start
    def counter():
        nonlocal count
        count += 1
        return count
    return counter

c = make_counter()
print(c())  # 1
print(c())  # 2
print(c())  # 3

# Logger
def make_logger(prefix):
    def log(message):
        print(f"[{prefix}] {message}")
    return log

error_log = make_logger("ERROR")
error_log("File not found")  # [ERROR] File not found
View Answer
#40
What is the difference between threading and multiprocessing in Python?

Multithreading (threading):

Threads operate within a single process and share the same memory space:

Python 3.13
import threading

def download(url):
    print(f"Downloading {url}")

threads = []
for url in ["url1", "url2", "url3"]:
    t = threading.Thread(target=download, args=(url,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()  # Wait for all threads to finish

Multiprocessing (multiprocessing):

Each process gets its own memory space and its own Python interpreter:

Python 3.13
from multiprocessing import Process

def heavy_computation(n):
    return sum(i * i for i in range(n))

processes = []
for n in [10_000_000, 20_000_000]:
    p = Process(target=heavy_computation, args=(n,))
    processes.append(p)
    p.start()

for p in processes:
    p.join()

The GIL (Global Interpreter Lock):

The GIL is a CPython mechanism that allows only one thread to execute Python bytecode at a time. As a result:

  • Threads do not speed up tasks performing heavy calculations (CPU-bound tasks).
  • Threads do speed up tasks waiting on external events (I/O-bound tasks): network requests, reading files.

When to use which:

  • threading — for I/O-bound tasks: file downloads, API calls, database queries.
  • multiprocessing — for CPU-bound tasks: data processing, complex mathematical calculations, and algorithms.
Featurethreadingmultiprocessing
MemorySharedSeparate
GIL impactConstrainedUnaffected
OverheadLowHigh
Best used forI/O-boundCPU-bound
View Answer