100+ Python Interview Questions and Answers With Code

Python interview questions with short answers and tested Python 3.14 examples: data types, functions, OOP, exceptions, generators, decorators, the GIL and free-threaded Python, NumPy, pandas and coding problems.

Python

Python interview questions test whether we can explain the core rules of the language in plain words, with a short answer and a small code snippet. The common topics are mutable and immutable types, argument passing, scope, classes, exceptions, generators, decorators and the GIL.

We use these questions to prepare for a fresher or junior interview with up to about three years of Python experience. Every answer starts with the short version that an interviewer expects, followed by an example.

Ten questions come up in almost every Python interview, and each short answer links to the full question.

  1. What is the difference between a list and a tuple? A list can change after we create it, whereas a tuple cannot, so only a tuple can be a dictionary key (question 2.3).
  2. What is the difference between == and is? The == operator checks whether two values are equal, whereas the is operator checks whether two names point to the same object (question 1.7).
  3. What are *args and **kwargs? The *args parameter collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dict (question 4.2).
  4. Does Python pass arguments by value or by reference? Neither. Python passes a reference to the object, so a function can change a mutable argument, such as a list, in place, but it cannot make the caller’s variable point to a new object (question 4.3).
  5. Why is a mutable default argument a bug? Python creates the default list only once, when it defines the function, so every call that uses the default shares the same list (question 4.4).
  6. What is a decorator? A decorator is a function that takes a function and returns a wrapped version of it, and we apply it with @name above the function (question 8.6).
  7. What is a generator? A generator is a function that uses yield to produce values one at a time, on demand (question 8.7).
  8. What is the difference between a shallow copy and a deep copy? A shallow copy copies only the outer object, so the copy shares the nested objects with the original. A deep copy copies the nested objects too (question 8.9).
  9. What is the GIL? The Global Interpreter Lock lets only one thread at a time run Python bytecode in CPython, although Python 3.14 officially supports an optional free-threaded build without the GIL (question 8.3).
  10. What does if __name__ == “__main__” do? The code under the __name__ check runs only when we run the file as a script, not when another module imports the file (question 6.2).

The following example shows five of these answers in code on Python 3.14, with the result of each line as a comment.

# 1. A tuple cannot change
fruits = ["apple", "banana"]
fruits.append("cherry")
point = (1, 2)
point[0] = 5                       # TypeError: 'tuple' object does not support item assignment

# 2. == compares values, is compares identity
a = [1, 2]
b = [1, 2]
same = a == b                      # True
identical = a is b                 # False

# 3. *args and **kwargs
def show(*args, **kwargs):
    return args, kwargs

result = show(1, 2, color="red")   # ((1, 2), {'color': 'red'})

# 5. The default list is shared between calls
def add_item(item, items=[]):
    items.append(item)
    return items

first = add_item("a")              # ['a']
second = add_item("b")             # ['a', 'b']

# 7. A generator produces values on demand
squares = (n * n for n in range(4))
values = list(squares)             # [0, 1, 4, 9]

Notice that the second add_item() call returns [‘a’, ‘b’], because both calls share one default list.

The remaining questions follow the topics of a typical interview, starting with basics, data types, strings, functions and scope, and moving on to object-oriented programming (OOP), modules and files, and exceptions. After that come the advanced topics (memory, the GIL, concurrency, decorators, generators and copying), followed by NumPy, pandas and coding problems.

1. Python Basics Interview Questions

Basic questions check how Python runs our code and how its syntax differs from Java or C++, so interviewers often start here with freshers.

1.1. Is Python an Interpreted or a Compiled Language?

Python is both compiled and interpreted. CPython, the standard Python implementation, first compiles the source file to bytecode and then interprets that bytecode in the Python virtual machine.

Bytecode is a list of simple instructions for the virtual machine, not machine code. The compile step happens when we run or import a file, so there is no separate build step as in Java or C++.

Python compiles the whole file before running it, so a syntax error stops the program before any line runs. In the next snippet, the first print() never prints, because the missing parenthesis on the second line stops the compile step.

print("start")
print("end"
SyntaxError: '(' was never closed

Other errors, such as TypeError or NameError, appear only at run time, when Python reaches the line. To catch them earlier, we use linters and type checkers such as ruff and mypy, which check the code before it runs.

CPython also has an experimental JIT (just-in-time) compiler, which turns often-used code into machine code while the program runs. The JIT arrived in 3.13, and the official Windows and macOS binaries of 3.14 include it, but it is off by default.

1.2. What Is a Dynamically Typed Language?

In a dynamically typed language, types are checked while the program runs, not before. The type belongs to the object, not to the variable, so the same variable can refer to an int and later to a str. Java and C++ are statically typed, which means we declare the type and the compiler checks it. Ruby and JavaScript are dynamically typed, like Python.

Python is also strongly typed, so it does not convert a str to an int for us, and adding the two raises TypeError.

x = 5
kind = type(x)                     # <class 'int'>
x = "five"
kind = type(x)                     # <class 'str'>
bad = "5" + 5                      # TypeError: can only concatenate str (not "int") to str

We do not declare variable types in Python. Type hints such as age: int are optional notes about the expected type, and Python ignores them at run time. Tools such as mypy read the hints and find mistakes before the code runs (question 4.9). Python itself accepts a str for an int hint.

age: int = "ten"                   # age = 'ten'

Dynamic typing costs some safety, because a wrong type shows up only when the line runs. For example, a form handler that gets “ten” as an age fails only when the code does math with it, which may happen in production. Type hints and tests find most type errors early.

1.3. What Is PEP 8?

PEP 8 is the official style guide for Python code, written by Guido van Rossum, Barry Warsaw and Alyssa Coghlan. When every developer follows PEP 8, all code looks the same and reviews go faster. Interviewers ask most often about indentation, line length, naming, blank lines and imports.

  • Indent with 4 spaces, never tabs.
  • Keep lines at most 79 characters long.
  • Use snake_case for functions and variables, CapWords for classes and UPPER_CASE for constants.
  • Put two blank lines around top-level functions and classes.
  • Put imports at the top of the file, one module per line.
MAX_RETRIES = 3

class OrderService:
    def total_price(self, prices):
        return sum(prices)

PEP 8 is a convention, not a language rule, so Python runs code that breaks it. Formatters and linters such as ruff and black apply the rules for us. A formatter rewrites the layout of our code, and a linter reports the lines that break the rules.

1.4. Is Indentation Required in Python?

Yes, Python uses indentation, not curly braces, to mark a block of code. A block follows if, for, while, def, class and similar statements, and a missing indent raises IndentationError.

age = 20
if age > 18:
print("adult")
IndentationError: expected an indented block after 'if' statement on line 2

Indenting the print() line by 4 spaces fixes the error. Python 3 also raises TabError when a block mixes tabs and spaces inconsistently. Every if-else statement and every for loop body follows the same indentation rule.

1.5. What Are Python Literals?

A literal is a fixed value that we write as is in the code, such as 5 in x = 5. Python has literals for numbers, text, booleans and None.

KindExamples
Integer10, 0x1F (hex), 0b101 (binary), 1_000_000
Float and complex3.14, 1e-3, 2j
String and bytes‘a’, “b”, “””c”””, f”{x}”, r”\n”, b”data”
BooleanTrue, False
NoneNone
hex_value = 0x1F                   # 31
binary = 0b101                     # 5
million = 1_000_000                # 1000000
z = 2 + 3j                         # (2+3j)

Number and string objects are immutable, which means they cannot change, but a variable is only a name for an object. So after x = 5, the statement x = 10 is valid, because the assignment makes the name x refer to a different object, and the object 5 stays unchanged.

1.6. What Are break, continue and pass in Python?

The break and continue statements change the normal flow of a loop, whereas pass does nothing and only fills a place where Python needs a statement.

  • The break statement ends the loop at once.
  • The continue statement skips the rest of the current iteration and starts the next one.
  • The pass statement does nothing. Python needs a statement in some places, for example in an empty function body, and pass fills that place until we write the real code.

For example, a loop that searches a list of orders for the first unpaid one uses break to stop at the match, and continue to skip the cancelled orders.

for i in range(10):
    if i == 5:
        continue
    print(i, end=" ")
0 1 2 3 4 6 7 8 9
for i in range(10):
    if i == 5:
        break
    print(i, end=" ")
0 1 2 3 4
def send_email(address):
    pass

result = send_email("a@example.com")   # None

1.7. What Is the Difference Between == and is?

The == operator checks whether two values are equal, whereas the is operator checks whether two names point to the same object in memory. So two separate lists with the same items are equal but not identical. Interviewers often ask about in and not in the same question. The in operator checks whether a collection, such as a list or a dict, contains an item, and not turns True into False and False into True.

a = [1, 2, 3]
b = [1, 2, 3]
same = a == b                      # True
identical = a is b                 # False
c = a
same_object = c is a               # True
found = 3 in a                     # True
missing = 10 not in a              # True
empty = not a                      # False
result = None
no_result = result is None         # True

We use is only for singletons such as None, and == for everything else. CPython caches small integers and some strings (it reuses the same object for them), so is on numbers or strings may give different results in different places.

1.8. How Do You Write Multi-Line Comments in Python?

Python has only single-line comments, which start with #. For several lines, we put # at the start of each line, and most editors do this with one shortcut.

# Calculate the total price
# including a 10% discount
total = 100 * 0.9                  # total = 90.0

Developers often use a triple-quoted string on its own line as a block comment. Python evaluates the string and throws the result away, so it acts like a comment, but it is not a real comment. As the first statement of a function or a class body, the string becomes the docstring (question 4.8), and the same applies at the top of a module.

1.9. What Do help() and dir() Do?

The help() function prints the documentation of an object, and the dir() function returns the names (attributes and methods) defined on it. Both functions help in the interactive shell when we explore a module we do not know.

import math
has_sqrt = "sqrt" in dir(math)     # True
last_three = dir("text")[-3:]      # ['translate', 'upper', 'zfill']
help(math.sqrt)
Help on built-in function sqrt in module math:

sqrt(x, /)
    Return the square root of x.

1.10. What Are the Differences Between Python 2 and Python 3?

Python 2 reached its end of life on January 1, 2020 (PEP 373), so new code targets Python 3 only. Interviewers still ask the question, because old tutorials and legacy code use Python 2 syntax.

FeaturePython 2Python 3
PrintingStatement: print “hi”Function: print(“hi”)
Textstr holds bytes, unicode holds textstr holds Unicode text, bytes holds bytes
Division7 / 2 gives 37 / 2 gives 3.5, 7 // 2 gives 3
Integersint and longOne int type of any size
Rangesrange() returns a list, xrange() is lazyrange() is lazy, xrange() is gone
Inputraw_input() returns a stringinput() returns a string
Exceptionsexcept ValueError, e:except ValueError as e:
Key lookupd.has_key(k)k in d
ClassesOld-style unless they inherit objectAll classes are new-style
Tabs and spacesMixing is allowedInconsistent mixing raises TabError
half = 7 / 2                         # 3.5
floor_half = 7 // 2                  # 3
big = 2 ** 100                       # 1267650600228229401496703205376
data = "cafe".encode()               # b'cafe'
stock = {"apple": 5}
found = "apple" in stock             # True
has_apple = stock.has_key("apple")   # AttributeError: 'dict' object has no attribute 'has_key'

Python 3.14 accepts except ValueError, TypeError: without parentheses (PEP 758), and that line catches both exception types. In Python 2, the same text meant “catch ValueError and store the exception in a variable named TypeError“.

1.11. What Is the Difference Between range and xrange?

The xrange() function existed only in Python 2. Python 3 removed it and made range() lazy, like the old xrange(). Lazy means that a range object does not build a list of numbers; it stores only start, stop and step and computes each number when we ask for it. So even a range of a trillion numbers is tiny, and it still supports len(), indexing, slicing and fast in checks.

import sys
r = range(0, 10, 2)
numbers = list(r)                    # [0, 2, 4, 6, 8]
size = len(r)                        # 5
part = r[1:3]                        # range(2, 6, 2)
big = range(10**12)
size_in_bytes = sys.getsizeof(big)   # 48
found = 999_999 in big               # True
old = xrange(5)                      # NameError: name 'xrange' is not defined

2. Python Data Types and Collections Questions

Questions about data types check whether we can pick the right collection for a job and whether we know which types can change.

2.1. What Are the Built-in Data Types in Python?

Python has built-in types for numbers, text, sequences, sets and mappings (types that store key-value pairs). In the table, each row is one category with its types and a literal example.

CategoryTypesExample
Numericint, float, complex42, 3.14, 2j
BooleanboolTrue
Textstr“hello”
Sequencelist, tuple, range[1, 2], (1, 2), range(3)
Binarybytes, bytearray, memoryviewb”abc”
Setset, frozenset{1, 2}
Mappingdict{“apple”: 5}
NoneNoneTypeNone
int_type = type(42)                # <class 'int'>
float_type = type(3.14)            # <class 'float'>
bool_type = type(True)             # <class 'bool'>
dict_type = type({"apple": 5})     # <class 'dict'>
none_type = type(None)             # <class 'NoneType'>
is_int = isinstance(True, int)     # True

The last line surprises many candidates, because bool is a subclass of int, so True == 1.

2.2. What Is the Difference Between Mutable and Immutable Types?

A mutable object can change after we create it, whereas an immutable object cannot. For an immutable object, every “change” creates a new object, so name.upper() returns a new string and leaves name unchanged.

MutableImmutable
list, dict, set, bytearray, most user classesint, float, bool, str, tuple, frozenset, bytes
name = "lokesh"
upper = name.upper()               # upper = 'LOKESH', name = 'lokesh'
scores = [90, 80]
scores.append(70)                  # scores = [90, 80, 70]
places = {(1, 2): "home"}
place = places[(1, 2)]             # 'home'
bad = {[1, 2]: "home"}             # TypeError: cannot use 'list' as a dict key (unhashable type: 'list')

Only hashable objects can be dictionary keys or set members. A hashable object has a hash value that never changes, and the immutable built-in types are hashable, so a tuple works as a dict key and a list does not. For example, a delivery app that caches distances by (from_city, to_city) uses a tuple as the dict key, because a list key raises TypeError.

2.3. What Is the Difference Between a List and a Tuple?

A list can change, and a tuple cannot. We use a list for a collection that grows or changes, and a tuple for a fixed group of values, such as a point (x, y) or a database row.

my_list = [1, 2, 3, "hello", True]
my_tuple = (1, 2, 3, "hello", True)
my_list[0] = 10                    # my_list = [10, 2, 3, 'hello', True]
my_tuple[0] = 10                   # TypeError: 'tuple' object does not support item assignment
ListTuple
Syntax[1, 2, 3](1, 2, 3), and (1,) for one item
MutableYes. Has append(), insert(), remove(), sort()No. Has only count() and index()
Hashable (dict key, set item)NoYes, when all items are hashable
MemoryLarger (spare room for growth)Smaller
Typical useItems of the same kind that changeA fixed record of different fields
import sys
list_size = sys.getsizeof([1, 2, 3])    # 88
tuple_size = sys.getsizeof((1, 2, 3))   # 72

A list keeps spare room so that append() stays fast, so the same three items need more memory in a list than in a tuple.

2.4. What Is the Difference Between an Array and a List?

A list holds objects of any type, whereas an array.array holds only numbers of one type. The array stores the numbers as raw C values, such as 32-bit integers, packed tightly in memory, so it uses less memory for large numeric data. Both are mutable and can grow. For math on arrays, we use NumPy instead.

array.arraylist
Item typesOne numeric type, set by a type code such as “i”Any mix of types
Mutable and growableYesYes
MemoryCompact, raw numbersOne pointer per item plus the objects
Importfrom array import arrayBuilt in
Typical useLarge lists of numbers, binary I/OEverything else
from array import array
nums = array("i", [1, 2, 3])
nums.append(4)                     # nums = array('i', [1, 2, 3, 4])
nums.append("x")                   # TypeError: 'str' object cannot be interpreted as an integer

2.5. What Is Slicing in Python?

Slicing takes a part of a sequence with seq[start:stop:step]. The item at start is included and the item at stop is not, and each of the three parts is optional. Slicing works on lists, tuples, strings and ranges, and it always returns a new object.

nums = [1, 2, 3, 4, 5]
middle = nums[1:4]                 # [2, 3, 4]
first_two = nums[:2]               # [1, 2]
tail = nums[3:]                    # [4, 5]
every_other = nums[::2]            # [1, 3, 5]
letters = "Hello, world!"[::2]     # 'Hlo ol!'
past_end = nums[1:100]             # [2, 3, 4, 5]

The last line shows that slicing never raises IndexError, because Python stops at the end of the list even when stop is past the end.

2.6. What Are Negative Indexes and What Does [::-1] Do?

A negative index counts from the end: -1 is the last item, and -2 is the one before it. So we get the last items without computing len(seq) – 1. A slice with step -1 walks backward, so [::-1] returns a reversed copy.

nums = [1, 2, 3, 4, 5]
last = nums[-1]                    # 5
second_last = nums[-2]             # 4
last_three = nums[-3:]             # [3, 4, 5]
reversed_text = "hello"[::-1]      # 'olleh'
reversed_nums = nums[::-1]         # [5, 4, 3, 2, 1]
word = "Hello, World!"[-6:-1]      # 'World'

Sets and dicts do not support indexes at all. A dict keeps insertion order, but we read its values by key, not by position.

2.7. What Are List, Dict and Set Comprehensions?

A comprehension builds a new list, dict or set in one line. The list comprehension pattern is [expression for item in iterable if condition], where an iterable is anything we can loop over, such as a list or a string. A comprehension is shorter than a for loop with append(), and often a little faster.

import math
squares = [x**2 for x in range(1, 6)]                         # squares = [1, 4, 9, 16, 25]
evens = [x for x in range(10) if x % 2 == 0]                  # evens = [0, 2, 4, 6, 8]
facts = {x**2: math.factorial(x) for x in range(1, 6)}        # facts = {1: 1, 4: 2, 9: 6, 16: 24, 25: 120}
letters = {c for c in "banana"}
unique = sorted(letters)                                      # ['a', 'b', 'n']
gen = (x**2 for x in range(3))
gen_type = type(gen)                                          # <class 'generator'>
values = tuple(gen)                                           # (0, 1, 4)

Python has no tuple comprehension, because parentheses create a generator expression instead. A generator expression produces values lazily, one at a time, and tuple() turns it into a tuple.

2.8. What Is the Difference Between remove(), pop() and del?

The remove() and pop() methods and the del statement all delete items from a list. The remove() method deletes by value, whereas pop() and del delete by position.

OperationRemoves byReturnsError when missing
lst.remove(x)Value (first match)NoneValueError
lst.pop(i)Index (the last item when we pass no index)The removed itemIndexError
del lst[i]Index or sliceNothing (a statement)IndexError
nums = [1, 2, 3, 2, 5]
nums.remove(2)                     # nums = [1, 3, 2, 5]
last = nums.pop()                  # last = 5, nums = [1, 3, 2]
del nums[0]                        # nums = [3, 2]
nums.remove(9)                     # ValueError: list.remove(x): x not in list

The del statement also deletes dict entries (del d[“key”]) and names (del x), and using a deleted name raises NameError. Since remove() deletes only the first match, we build a new list to remove every copy of a value, such as [n for n in nums if n != 2].

2.9. What Is the Difference Between append() and extend()?

The append() method adds its argument as one item, whereas extend() adds every item of an iterable. So append([3, 4]) adds one list inside the list, and extend([3, 4]) adds two numbers.

a = [1, 2]
a.append([3, 4])                   # a = [1, 2, [3, 4]]
b = [1, 2]
b.extend([3, 4])                   # b = [1, 2, 3, 4]
c = b + [5]                        # [1, 2, 3, 4, 5]

2.10. Are Python Dictionaries Ordered?

Yes, since Python 3.7 a dict keeps keys in the order we added them, and the language guarantees that order. Before 3.7, we needed an OrderedDict for this.

Today, OrderedDict is useful mainly for two features. Its move_to_end() method moves a key to either end, and two OrderedDict objects are equal only when their keys are in the same order.

ages = {}
ages["Lokesh"] = 37
ages["Alex"] = 25
ages["Brian"] = 30
names = list(ages)                   # ['Lokesh', 'Alex', 'Brian']
john = ages.get("John")              # None
john_or_zero = ages.get("John", 0)   # 0
john_age = ages["John"]              # KeyError: 'John'

A set does not keep insertion order.

2.11. What Is the Difference Between sort() and sorted()?

The list.sort() method changes the list itself and returns None, whereas the sorted() function returns a new sorted list. The sorted() function also accepts any iterable, such as a string or a tuple. Both use Timsort, a mix of merge sort and insertion sort, which is stable, so items with equal keys keep their original order.

nums = [3, 1, 2]
result = nums.sort()                       # result = None, nums = [1, 2, 3]
letters = sorted("cba")                    # ['a', 'b', 'c']
words = ["banana", "kiwi", "apple"]
by_length = sorted(words, key=len)         # ['kiwi', 'apple', 'banana']
descending = sorted(words, reverse=True)   # ['kiwi', 'banana', 'apple']

2.12. What Is Type Conversion in Python?

Type conversion changes a value from one type to another. Python converts types on its own only between number types, so 1 + 2.5 turns 1 into a float. For every other conversion, we call a constructor such as int(), float(), str(), list(), tuple(), set() or dict().

number = int("10")                            # 10
text = str(10)                                # '10'
price = float("3.5")                          # 3.5
whole = int(3.9)                              # 3
mixed = 1 + 2.5                               # 3.5
chars = list("abc")                           # ['a', 'b', 'c']
stock = dict([("apple", 5), ("banana", 3)])   # {'apple': 5, 'banana': 3}
bad = int("abc")                              # ValueError: invalid literal for int() with base 10: 'abc'

The call int(3.9) cuts off the decimal part and moves toward zero, so it returns 3. To round instead, we use round().

User input is the common source of bad text. For example, the quantity field of an order form can contain “abc”, so we catch ValueError and fall back to a default instead of crashing the request.

def to_int(text, default=0):
    try:
        return int(text)
    except (TypeError, ValueError):
        return default

qty = to_int(" 12 ")               # 12
bad_qty = to_int("abc")            # 0
no_qty = to_int(None)              # 0

2.13. What Does the zip() Function Do?

The zip() function pairs items from two or more iterables by position and returns an iterator of tuples. When the inputs have different lengths, zip() stops at the shortest one. With strict=True (Python 3.10 and later), it raises ValueError instead.

names = ["apple", "banana", "cherry"]
prices = [5, 3, 7]
pairs = list(zip(names, prices))                    # [('apple', 5), ('banana', 3), ('cherry', 7)]
price_of = dict(zip(names, prices))                 # {'apple': 5, 'banana': 3, 'cherry': 7}
short = list(zip([1, 2, 3], "ab"))                  # [(1, 'a'), (2, 'b')]
checked = list(zip([1, 2, 3], "ab", strict=True))   # ValueError: zip() argument 2 is shorter than argument 1

2.14. What Does the enumerate() Function Do?

The enumerate() function gives us the index and the item together in a loop. Each step returns a pair (index, item), so we do not need a counter variable or range(len(seq)).

fruits = ["apple", "banana", "orange"]
for index, fruit in enumerate(fruits, start=1):
    print(index, fruit)
1 apple
2 banana
3 orange

3. Python String Interview Questions

A Python string is an immutable sequence of Unicode characters, so every string method returns a new string and leaves the original unchanged.

3.1. How Do split() and join() Work?

The split() method cuts a string into a list of parts, and the join() method glues a list of strings back into one string. Both use a separator, such as a comma. Without an argument, split() splits the string at any run of whitespace.

parts = "Hello, world!".split(",")          # ['Hello', ' world!']
words = "a  b   c".split()                  # ['a', 'b', 'c']
date_parts = "2026-10-04".split("-", 1)     # ['2026', '10-04']
greeting = ", ".join(["Hello", "world!"])   # 'Hello, world!'
date = "-".join(["2026", "10", "04"])       # '2026-10-04'
bad = " ".join([1, 2])                      # TypeError: sequence item 0: expected str instance, int found

We call join() on the separator string, not on the list. Every item must already be a string, otherwise join() raises TypeError, so for numbers we convert them first with ” “.join(map(str, nums)).

3.2. How Do You Change the Case of a String?

Python strings have one method for each kind of case change, and each method returns a new string.

MethodResult for “hELLO wORLD”
lower()“hello world”
upper()“HELLO WORLD”
capitalize()“Hello world” (first letter only, rest lowercase)
title()“Hello World”
swapcase()“Hello World” (each letter flipped)
casefold()“hello world” (a stronger lowercase, made for comparisons)
s = "hELLO wORLD"
lower = s.lower()                    # 'hello world'
upper = s.upper()                    # 'HELLO WORLD'
capitalized = s.capitalize()         # 'Hello world'
title = s.title()                    # 'Hello World'
swapped = s.swapcase()               # 'Hello World'
flipped = "HeLLo WoRLd".swapcase()   # 'hEllO wOrlD'
folded = "stra\u00dfe".casefold()    # 'strasse'

For case-insensitive comparisons, casefold() is safer than lower(), because it also converts special letters; for example, the German sharp s (code point U+00DF) becomes “ss”. For example, a search box that compares names with casefold() finds “Strasse” when the user types “STRASSE”. To change only the first letter, we call capitalize(), which also lowercases the rest.

3.3. What Are f-strings and t-strings?

An f-string (f”…”) puts the values of variables and expressions into a string. Python evaluates each expression inside {} and returns a normal str. After a colon, we can add a format specifier, such as ,.2f for two decimals, and a = after the expression prints the expression together with its value, which helps with debugging.

name = "Lokesh"
price = 1234.5
greeting = f"Hi {name}"            # 'Hi Lokesh'
amount = f"{price:,.2f}"           # '1,234.50'
debug = f"{price=}"                # 'price=1234.5'
padded = f"{name:>10}"             # '    Lokesh'

Python 3.14 added t-strings (t”…”, PEP 750), which do not build a string at all but return a Template object. The Template keeps the fixed text and the values apart, so a library can make the values safe before joining them, for example for HTML or SQL.

name = "<b>Lokesh</b>"
tmpl = t"Hi {name}"
kind = type(tmpl)                  # <class 'string.templatelib.Template'>
strings = tmpl.strings             # ('Hi ', '')
values = tmpl.values               # ('<b>Lokesh</b>',)

3.4. What Do split(), sub() and subn() Do in the re Module?

The re module works with regular expressions, which are patterns that describe text, such as “a comma or a space”. Its split() function cuts text at a pattern, whereas sub() and subn() replace the matches, and subn() also counts the replacements.

  • The call re.split(pattern, s) splits a string at every match of the pattern.
  • The call re.sub(pattern, repl, s) replaces every match and returns the new string.
  • The call re.subn(pattern, repl, s) replaces every match too, but returns a tuple (new_string, count).
import re
text = "The,quick,brown fox jumps over the lazy dog"
words = re.split(r"[,\s]", text)        # ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog']
replaced = re.sub("fox", "cat", text)   # 'The,quick,brown cat jumps over the lazy dog'
result = re.subn("o", "0", text)        # ('The,quick,br0wn f0x jumps 0ver the lazy d0g', 4)

The str.split() method accepts only one fixed separator, whereas re.split() accepts a pattern, so one call splits on both commas and spaces. For example, a tag field where users type “java, spring boot;kafka” splits into three clean tags with re.split(r”[,;]\s*”, text).

4. Functions and Scope Interview Questions

Function questions check how Python passes arguments and how default values behave. Interviewers also ask where Python looks up a name, which is the scope question.

4.1. How Do You Define and Call a Function in Python?

A function is a named block of code that we can run again and again. We define it with def, and it can take positional and keyword arguments with default values. A function without a return statement returns None. Python also has small functions without a name, written with lambda (question 4.5).

def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

hello = greet("Lokesh")            # 'Hello, Lokesh!'
hi = greet("Alex", greeting="Hi")  # 'Hi, Alex!'
nobody = greet()                   # TypeError: greet() missing 1 required positional argument: 'name'

Python also comes with many built-in functions, such as print(), len(), range(), sorted() and sum().

4.2. What Are *args and **kwargs?

The *args parameter collects any number of extra positional arguments into a tuple, and **kwargs collects any number of extra keyword arguments into a dict. The names args and kwargs are only a convention, because only the single and double stars matter. When we call a function, the same stars unpack a list or a dict into arguments.

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

def describe(**kwargs):
    return kwargs

summed = total(1, 2, 3)                  # 6
person = describe(name="John", age=30)   # {'name': 'John', 'age': 30}
nums = [1, 2, 3]
unpacked = total(*nums)                  # 6

The order in a function signature is fixed. Regular parameters come first, then *args, then keyword-only parameters, and **kwargs comes last. The same star also unpacks a variable-length tuple in an assignment, such as first, *rest = (1, 2, 3).

4.3. Does Python Pass Arguments by Value or by Reference?

Neither. Python passes a reference to the object. People often call this model “pass by object reference” or “pass by assignment”. Inside the function, the parameter becomes a second name for the caller’s object, and two rules follow from that.

  • If the function changes a mutable object in place, for example with append() or item assignment, the caller sees the change.
  • If the function assigns a new object to the parameter, only the local name changes, and the caller’s variable still points to the old object.
def add_item(items):
    items.append(4)

def replace(items):
    items = [0]

nums = [1, 2, 3]
add_item(nums)                     # nums = [1, 2, 3, 4]
replace(nums)                      # nums = [1, 2, 3, 4]

Numbers, strings and tuples are immutable and cannot change in place, so a function can never change the caller’s int or str. A list is different. For example, a helper that adds a discount line to an order’s item list changes the caller’s list, which surprises a caller who expected a copy.

4.4. Why Are Mutable Default Arguments a Problem?

Python creates a default value only once, when it runs the def statement, not on every call. So a mutable default, such as [] or {}, is shared by all calls that use the default. In a web app, the shared list survives between requests, so tags from one request show up in the response to the next one.

def add_tag(tag, tags=[]):
    tags.append(tag)
    return tags

first = add_tag("python")          # ['python']
second = add_tag("java")           # ['python', 'java']

The fix is to use None as the default and create a new object inside the function.

def add_tag(tag, tags=None):
    if tags is None:
        tags = []
    tags.append(tag)
    return tags

first = add_tag("python")          # ['python']
second = add_tag("java")           # ['java']

4.5. What Is a Lambda Function?

A lambda is a small function without a name, written in one line as lambda arguments: expression. We use lambdas for short callbacks (functions that we pass to another function), most often as the key of functions such as sorted() and max(), and also in map() and filter().

A lambda holds one expression only, so it cannot contain statements such as loops or return.

from functools import reduce
prices = {"apple": 5, "banana": 3, "cherry": 7}
by_price = sorted(prices, key=lambda k: prices[k])   # ['banana', 'apple', 'cherry']
doubled = list(map(lambda x: x * 2, [1, 2, 3]))      # [2, 4, 6]
bigger = list(filter(lambda x: x > 1, [1, 2, 3]))    # [2, 3]
product = reduce(lambda a, b: a * b, [1, 2, 3, 4])   # 24

The map() function applies a function to every item, and filter() keeps the items for which the function returns true. The reduce() function (in functools) combines all items into one value, for example by multiplying them. A comprehension is often easier to read than map() or filter() with a lambda.

4.6. What Are Namespaces and Scope in Python?

A namespace is a table of names and the objects they point to, and a scope is the part of the code where a namespace is visible. Python looks up a name in four scopes, in a fixed order called the LEGB rule.

  1. Local scope holds the names assigned inside the current function.
  2. Enclosing scope holds the names in the outer function, for nested functions.
  3. Global scope holds the names assigned at the top level of the module.
  4. Built-in scope holds names such as len and print.

If no scope has the name, Python raises NameError. Each function call creates a new local namespace, which disappears when the call returns.

x = "global"

def outer():
    x = "enclosing"
    def inner():
        return x
    return inner()

found = outer()                    # 'enclosing'
size = len("abc")                  # 3
value = undefined_name             # NameError: name 'undefined_name' is not defined

Unlike Java, Python has no block scope, so a variable assigned inside an if or for block is still visible after the block, in the same function. The globals() and locals() functions return the global and local namespaces as dicts.

4.7. What Is the Difference Between Local and Global Variables?

A local variable exists only during one function call, whereas a global variable is defined at the top level of the module and is visible in all its functions. When a function assigns to a name, the name becomes local, even if a global with the same name exists. So to assign a new value to a global, the function must declare the name global, and to assign to a variable of the enclosing function, the inner function declares it nonlocal.

count = 0

def shadow():
    count = 5
    return count

def increment():
    global count
    count += 1

def make_counter():
    n = 0
    def step():
        nonlocal n
        n += 1
        return n
    return step

local = shadow()                   # 5
before = count                     # 0
increment()                        # count = 1
counter = make_counter()
first = counter()                  # 1
second = counter()                 # 2

Without the global line, count += 1 raises UnboundLocalError, because the assignment makes count local to the function and the local count has no value yet. So we keep global variables to a minimum and pass values as arguments instead.

4.8. What Is a Docstring in Python?

A docstring is the documentation string of a module, class or function. We write it as the first statement of the body, and Python stores it in the __doc__ attribute. The help() function shows the docstring, and tools such as Sphinx build documentation from docstrings. By convention, docstrings use triple double quotes.

def area(width, height):
    """Return the area of a rectangle."""
    return width * height

doc = area.__doc__                 # 'Return the area of a rectangle.'

4.9. What Are Type Hints in Python?

Type hints are optional notes that say which types a function expects and returns. Hints can also describe variables. Python stores the hints but does not check them at run time, whereas static checkers such as mypy and IDEs read them and warn us about mistakes. Since Python 3.14, Python evaluates annotations lazily, only when some code reads them (PEP 649), so a hint can name a class that is defined later in the file.

def total(prices: list[float], tax: float = 0.1) -> float:
    return sum(prices) * (1 + tax)

amount = round(total([10.0, 20.0]), 2)   # 33.0
hints = total.__annotations__            # {'prices': list[float], 'tax': <class 'float'>, 'return': <class 'float'>}

5. Object-Oriented Programming (OOP) Questions

Python supports classes, inheritance and polymorphism, but it handles access control and overloading differently from Java, and interviewers focus on those differences.

5.1. What Is __init__ and What Is self?

The __init__ method sets up a new object right after Python creates it, and self is that new object. Python passes self as the first argument of every instance method, so we never pass it ourselves, and the name self is only a convention. Many people call __init__ the constructor, but strictly, __new__ creates the object (question 5.2).

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def describe(self):
        return f"{self.year} {self.make} {self.model}"

my_car = Car("Toyota", "Corolla", 2021)
make = my_car.make                 # 'Toyota'
text = my_car.describe()           # '2021 Toyota Corolla'
car = Car("Honda")                 # TypeError: Car.__init__() missing 2 required positional arguments: 'model' and 'year'

5.2. What Is the Difference Between __new__ and __init__?

The __new__ method creates the new object, and then __init__ fills it with data. Python treats __new__ as a static method that receives the class and returns the new instance, whereas __init__ receives the instance and returns None. We override __new__ only in special cases, such as singletons (classes with only one instance) and subclasses of immutable types such as int and str.

class Point:
    def __new__(cls, *args):
        print("1. __new__ creates the object")
        return super().__new__(cls)

    def __init__(self, x, y):
        print("2. __init__ sets the attributes")
        self.x, self.y = x, y

p = Point(1, 2)
1. __new__ creates the object
2. __init__ sets the attributes

5.3. Can We Overload Constructors or Methods in Python?

No, Python does not support overloading. Overloading means several methods with the same name and different parameters, and Java chooses between overloads at compile time. In Python, a second def with the same name replaces the first one, because Python looks up a method by its name only.

class Person:
    def __init__(self, name, age):
        self.name, self.age = name, age

    def __init__(self, name):
        self.name, self.age = name, 0

age = Person("Bob").age            # 0
alice = Person("Alice", 25)        # TypeError: Person.__init__() takes 2 positional arguments but 3 were given

We can get the same effect in three ways.

  • Default arguments, such as def __init__(self, name, age=0).
  • Class methods as named alternative constructors, such as Person.from_dict(data) (question 5.4).
  • The functools.singledispatchmethod decorator, which picks an implementation by the type of the first argument.
class Person:
    def __init__(self, name, age=0):
        self.name, self.age = name, age

alice_age = Person("Alice", 25).age   # 25
bob_age = Person("Bob").age           # 0

5.4. What Is the Difference Between Instance, Class and Static Methods?

Python has three kinds of methods, and they differ in what they receive as the first argument.

KindDecoratorFirst argumentTypical use
Instance methodnoneself (the object)Work with one object’s data
Class method@classmethodcls (the class)Alternative constructors, class-wide data
Static method@staticmethodnoneA helper that belongs to the class but needs neither the object nor the class
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    def fahrenheit(self):
        return self.celsius * 9 / 5 + 32

    @classmethod
    def from_fahrenheit(cls, f):
        return cls((f - 32) * 5 / 9)

    @staticmethod
    def is_freezing(celsius):
        return celsius <= 0

boiling_f = Temperature(100).fahrenheit()              # 212.0
boiling_c = Temperature.from_fahrenheit(212).celsius   # 100.0
freezing = Temperature.is_freezing(-5)                 # True

5.5. Can We Call a Parent Class Method Without Creating an Instance?

Yes, in two cases. First, we call class methods and static methods on the class itself, without any object. Second, a subclass calls a parent’s method on its own object with super(). An instance method still needs some object as self, but super() means the parent part of the current object, so we never create a separate parent object.

class Animal:
    count = 0

    @classmethod
    def describe(cls):
        return f"{cls.__name__} class, {cls.count} created"

    def speak(self):
        return "..."

class Dog(Animal):
    def speak(self):
        return super().speak() + " Woof!"

info = Animal.describe()           # 'Animal class, 0 created'
sound = Dog().speak()              # '... Woof!'

5.6. How Do You Create an Empty Class in Python?

An empty class has pass as its body. Python needs at least one statement in a block, and pass is that statement. Empty classes are common as custom exception types and as placeholders, and we can still add attributes to an instance later.

class Order:
    pass

class PaymentError(Exception):
    pass

order = Order()
order.total = 99
total = order.total                # 99

5.7. What Does the object() Function Do?

The object() call returns a new, empty instance of the class object, which is the base class of every Python class. In Python 3, every class inherits from object even when we do not list it, so class MyClass(object): is the same as class MyClass:. The longer form is a habit from Python 2, where it was needed for new-style classes.

An object() instance has no __dict__, so we cannot add attributes to it. Its common use is a unique “sentinel” value, a marker value that no caller can pass by accident. For example, a cache lookup that must tell “no value stored” apart from a stored None uses a sentinel as the default.

class MyClass:
    pass

mro = MyClass.__mro__                    # (<class '__main__.MyClass'>, <class 'object'>)
MISSING = object()
MISSING.name = "x"                       # AttributeError: 'object' object has no attribute 'name' and no __dict__ for setting new attributes

def get(d, key, default=MISSING):
    if key in d:
        return d[key]
    if default is MISSING:
        raise KeyError(key)
    return default

pear = get({"apple": 5}, "pear", None)   # None

5.8. How Do You Check if a Class Is a Child of Another Class?

The issubclass(child, parent) function checks two classes, and the isinstance(obj, cls) function checks an object. Both return True for indirect subclasses too, such as a grandchild class, and both accept a tuple of classes.

class Animal: pass
class Dog(Animal): pass
class GoldenRetriever(Dog): pass

dog_is_animal = issubclass(Dog, Animal)                  # True
golden_is_animal = issubclass(GoldenRetriever, Animal)   # True
animal_is_dog = issubclass(Animal, Dog)                  # False
golden_is_dog = isinstance(GoldenRetriever(), Dog)       # True
is_number = isinstance(5, (int, float))                  # True

5.9. Does Python Support Multiple Inheritance?

Yes, a class can list several base classes and inherits the methods of all of them. When two parents define the same method, Python picks one by the method resolution order (question 5.10).

The MRO also solves the diamond problem, where a class inherits the same grandparent through two parents. Still, many teams prefer small “mixin” classes or composition over deep multiple inheritance.

class Flyer:
    def fly(self):
        return "flying"

class Swimmer:
    def swim(self):
        return "swimming"

class Duck(Flyer, Swimmer):
    pass

duck = Duck()
flying = duck.fly()                # 'flying'
swimming = duck.swim()             # 'swimming'

5.10. What Is the MRO (Method Resolution Order) in Python?

The MRO is the order in which Python searches a class and its parents for a method or attribute. Python computes it with the C3 linearization algorithm, which follows three rules.

  • A child comes before its parents.
  • The parents keep the order in which the class lists them.
  • Every class appears once.

The __mro__ attribute and the mro() method show the MRO of a class.

class A:
    def who(self):
        return "A"

class B(A):
    def who(self):
        return "B"

class C(A):
    def who(self):
        return "C"

class D(B, C):
    pass

order = [cls.__name__ for cls in D.mro()]   # ['D', 'B', 'C', 'A', 'object']
winner = D().who()                          # 'B'

D defines no who(), so Python checks B next and uses B.who(). The super() call also follows the MRO, so when the object is a D, super() in B calls C, not A.

5.11. What Is Polymorphism in Python?

Polymorphism means that the same call works on objects of different classes, and each class runs its own version of the method. In Python, polymorphism comes from method overriding and from duck typing, which means that a function accepts any object that has the needed method, whatever its class. Method overloading is not involved, because Python has no overloading (question 5.3).

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

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

class Robot:
    def speak(self):
        return "Beep!"

sounds = [pet.speak() for pet in (Dog(), Cat(), Robot())]   # ['Woof!', 'Meow!', 'Beep!']
sizes = len("abc"), len([1, 2])                             # (3, 2)

Robot shares no base class with Dog and Cat, yet the loop works, because only the speak() method matters. Built-in functions such as len() are polymorphic in the same way.

5.12. Does Python Have new and override Modifiers?

No, new and override are C# keywords, and Python has no modifiers for methods. A subclass overrides a method by defining a method with the same name, and it calls the parent version with super().

Since Python 3.12, the @typing.override decorator (PEP 698) marks a method as an override, so type checkers report a typo in the method name. Python itself does not check the decorator at run time.

from typing import override

class Account:
    def fee(self):
        return 10

class PremiumAccount(Account):
    @override
    def fee(self):
        return super().fee() // 2

fee = PremiumAccount().fee()       # 5

5.13. What Is Encapsulation and Does Python Have Access Specifiers?

Encapsulation means that other code uses an object through its methods, not through its internal fields. Python has no public, protected or private keywords, so it uses naming conventions instead.

NameMeaningEnforced?
namePublicNo
_nameProtected: internal, do not use from outsideNo, only a convention
__namePrivate. Python renames the attribute to _ClassName__name (name mangling)Partly. The name changes
Module-level nameGlobal to the moduleNo
class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self._branch = "Delhi"
        self.__balance = balance

    def balance(self):
        return self.__balance

acc = Account("Lokesh", 100)
owner = acc.owner                  # 'Lokesh'
branch = acc._branch               # 'Delhi'
balance = acc.balance()            # 100
hidden = acc.__balance             # AttributeError: 'Account' object has no attribute '__balance'
mangled = acc._Account__balance    # 100

Name mangling exists to avoid clashes with attribute names in subclasses, not as a security feature. For read-only or validated attributes, we use @property.

5.14. How Do You Do Data Abstraction in Python?

Data abstraction hides how something works and shows only what it does. In Python, we declare the interface with an abstract base class from the abc module. The class inherits from ABC and marks methods with @abstractmethod. Creating an object of an abstract class raises TypeError, so each subclass must implement the abstract methods.

from abc import ABC, abstractmethod
import math

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

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

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

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

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

rect_area = Rectangle(5, 10).area()   # 50
circle_area = Circle(7).area()        # 153.94
shape = Shape()                       # TypeError: Can't instantiate abstract class Shape without an implementation for abstract method 'area'

Callers work with Shape and call area(), so they do not need to know the formula behind each shape.

5.15. What Is the Difference Between __str__ and __repr__?

The __str__ method returns a readable text for users, whereas __repr__ returns an exact text for developers. The print() and str() functions use __str__, whereas the REPL, repr() and containers such as lists use __repr__. When a class defines only __repr__, Python uses it for str() too.

Both are dunder (double underscore) methods, which Python calls for us when we use built-in operations. For example, len() calls __len__, == calls __eq__ and + calls __add__.

class Book:
    def __init__(self, title, pages):
        self.title, self.pages = title, pages

    def __str__(self):
        return self.title

    def __repr__(self):
        return f"Book({self.title!r}, {self.pages})"

    def __len__(self):
        return self.pages

book = Book("Python 101", 250)
text = str(book)                   # 'Python 101'
debug = repr(book)                 # "Book('Python 101', 250)"
books = [book]                     # [Book('Python 101', 250)]
pages = len(book)                  # 250

5.16. What Is a Data Class?

A data class is a class that holds data, and Python writes the standard methods for it. We mark the class with @dataclass and list its fields with types, and Python generates __init__, __repr__ and __eq__ from those fields, so we skip a lot of repeated code. With frozen=True, the objects cannot change and are hashable, and the dataclasses module has more options, such as order=True and slots=True.

from dataclasses import dataclass, field

@dataclass(frozen=True)
class Point:
    x: int
    y: int = 0

@dataclass
class Cart:
    items: list = field(default_factory=list)

p = Point(1, 2)                    # Point(x=1, y=2)
equal = Point(1) == Point(1, 0)    # True
Point(1, 2).x = 5                  # FrozenInstanceError: cannot assign to field 'x'
cart = Cart()                      # Cart(items=[])

The call field(default_factory=list) creates a new list for every object, so the data class avoids the shared mutable default from question 4.4. For a plain [] default, @dataclass raises ValueError.

5.17. What Is __del__ and Why Is It Rarely Used for Cleanup?

The __del__ method runs when Python is about to destroy an object. Python calls __del__ a finalizer, and Java’s finalize() played the same role.

Python does not promise when __del__ runs, and at interpreter exit it may not run at all. So we do not rely on __del__ to close files or connections, and we use a with statement (question 8.8) or try/finally for cleanup.

class Resource:
    def __del__(self):
        print("cleaned up")

r = Resource()
del r
cleaned up

In CPython, del r removed the last reference, so Python destroyed the object at once. With reference cycles (groups of objects that point to each other) and on other implementations such as PyPy, the call happens later.

5.18. Is Python Fully Object-Oriented?

Yes, in the sense that everything in Python is an object, including numbers, functions, classes and modules. But Python does not force us to write classes, so we can write procedural code with plain functions, or functional code with map(), filter() and lambdas. For object-oriented code, Python supports inheritance, polymorphism, encapsulation by convention and abstraction through abc.

int_is_object = isinstance(5, object)    # True
bits = (5).bit_length()                  # 3
def f(): pass
func_is_object = isinstance(f, object)   # True
meta = type(int)                         # <class 'type'>

6. Modules, Packages, Files and Tools

Module questions check how Python finds our code and how a project manages its libraries. Interviewers also ask how we read files, including files larger than memory.

6.1. What Are Modules and Packages in Python?

A module is one .py file, and a package is a folder of modules. In most projects, a package folder has an __init__.py file and can contain sub-packages, and we load both with import.

The first import runs the module once and stores the result in sys.modules, and later imports reuse that copy. Since Python 3.3, a folder without __init__.py also works as a namespace package.

shop/
    __init__.py
    cart.py
    payments/
        __init__.py
        card.py
import math
from math import sqrt
from collections import Counter as C
import json as js

pi = math.pi                       # 3.141592653589793
root = sqrt(16)                    # 4.0
top = C("banana").most_common(1)   # [('a', 3)]
text = js.dumps({"apple": 5})      # '{"apple": 5}'

With the shop layout, the statement from shop.payments.card import charge imports a function from a sub-package. We install third-party packages such as NumPy and Flask with pip (question 6.5).

6.2. What Does if __name__ == “__main__” Do?

The __name__ check lets a file run its main code only when we run the file as a script. Every module has a __name__ variable. For the file we run, Python sets it to “__main__”, and for an imported file, Python sets it to the module name. So one file works both as a script and as a module that others import, and on import, the script part does not run. For example, a report.py file can define helper functions that a web app imports, and still print a report when we run it from a scheduled job.

def main():
    print("Running as a script")

print("__name__ is", __name__)
if __name__ == "__main__":
    main()
__name__ is __main__
Running as a script

When another file runs import greet, the output is only __name__ is greet, and main() does not run.

6.3. What Is PYTHONPATH?

PYTHONPATH is an environment variable that lists extra folders where Python looks for modules. Python builds its search list, sys.path, in a fixed order. First comes the folder of the script, then the PYTHONPATH folders, then the standard library and site-packages (the folder where pip installs packages). We rarely need PYTHONPATH, because the usual way is to install our own project with pip install -e . inside a virtual environment.

# Linux and macOS
export PYTHONPATH=/opt/mylibs
python3 -c "import sys; print(sys.path[1])"
# /opt/mylibs

# Windows (cmd)
set PYTHONPATH=C:\mylibs

6.4. What Is the Difference Between .py and .pyc Files?

A .py file holds our source code, and a .pyc file holds the bytecode that CPython compiled from that source. CPython writes .pyc files for imported modules into a __pycache__ folder, with the interpreter version in the file name, such as greet.cpython-314.pyc. On the next import, Python skips compiling if the source has not changed.

.py.pyc
ContentSource code, human-readableBytecode, binary
Created byThe developerCPython, on import
LocationProject folders__pycache__/ next to the source
PortabilityAny Python 3 version that supports the syntaxSame Python minor version only. Works on any OS
SpeedSame run speedFaster start-up only, because the compile step is skipped. The code does not run faster
python3 -m py_compile greet.py
ls __pycache__
# greet.cpython-314.pyc

6.5. What Are pip and Virtual Environments?

The pip tool installs packages, and a virtual environment keeps each project’s packages separate. The pip tool downloads the packages from PyPI, the Python Package Index. A virtual environment is a folder with its own link to the interpreter and its own packages, so when each project gets its own environment, two projects can use different versions of the same library.

python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install requests
pip show requests | grep Requires
# Requires: certifi, charset_normalizer, idna, urllib3
pip freeze > requirements.txt

The Requires line shows the transitive dependencies, the packages that requests itself needs, which pip installed along with it.

To get the same versions on every machine, we pin all versions with pip freeze into requirements.txt. A lock file from a tool such as uv or Poetry does the same job. Without a virtual environment, upgrading a library for one project can break another project on the same laptop.

6.6. What Is the Shortest Way to Read and Print a Text File?

The safe short way is with open(…), because the with block always closes the file, even when an error happens. For a one-liner, we use pathlib.

from pathlib import Path
with open("notes.txt", encoding="utf-8") as f:
    content = f.read()             # content = 'Buy milk\nCall Alex\n'

lines = Path("notes.txt").read_text(encoding="utf-8").splitlines()  # ['Buy milk', 'Call Alex']

We pass encoding=”utf-8″ because the default encoding depends on the operating system. For large files, we read the file line by line instead (question 6.7).

When the file may not exist, such as a config file that the user has not created yet, we catch FileNotFoundError and use a default.

try:
    settings = Path("settings.txt").read_text(encoding="utf-8")
except FileNotFoundError:
    settings = ""                  # settings = ''

6.7. How Do You Read an 8 GB File in Python?

We read a big file in small pieces, never all at once with read(), so memory use stays small whatever the file size. For a text file, a for loop reads one line at a time through an internal buffer, and for a binary file, we read chunks of a fixed size.

line_count = 0
with open("big.txt", encoding="utf-8") as f:
    for line in f:
        line_count += 1

total_bytes = 0
with open("big.txt", "rb") as f:
    while chunk := f.read(1024 * 1024):
        total_bytes += len(chunk)

print(line_count)                  # 6
print(total_bytes)                 # 78

The := (walrus) operator assigns the chunk and tests it in one expression. Another option is the mmap module, which maps the file into virtual memory, so the operating system loads only the pages that our code accesses. That way, a 64-bit process can work with a file larger than RAM. For example, a nightly job that counts the lines of an 8 GB access log needs only the memory of one buffer.

6.8. What Are Unit Tests in Python?

A unit test is a small automatic test that runs one piece of code, such as a function, in isolation. Python comes with unittest, a JUnit-style framework with test classes, but most projects use pytest, which runs plain functions with assert.

Good unit tests are fast and independent of each other. They avoid real databases and network calls, and unittest.mock replaces those calls with fake objects.

import pytest

def apply_discount(price, percent):
    if not 0 <= percent <= 100:
        raise ValueError("percent must be 0-100")
    return round(price * (1 - percent / 100), 2)

def test_discount():
    assert apply_discount(200, 10) == 180.0

def test_invalid_percent():
    with pytest.raises(ValueError):
        apply_discount(200, 150)
pytest -q test_prices.py
# ..                                                                       [100%]
# 2 passed in 0.00s

6.9. What Is Tkinter?

Tkinter is the GUI library that comes with Python. A GUI is a graphical user interface with windows and buttons. Tkinter is a wrapper around the Tcl/Tk toolkit and runs on Windows, macOS and Linux.

Tkinter provides widgets such as windows, labels, buttons and text fields. A Tkinter program is event-driven, so mainloop() waits for clicks and key presses and calls our handler functions.

import tkinter as tk

root = tk.Tk()
root.title("Hello")
tk.Label(root, text="Hello, Tkinter!").pack(padx=20, pady=10)
tk.Button(root, text="Close", command=root.destroy).pack(pady=10)
root.after(500, root.destroy)
root.mainloop()

The root.after(500, root.destroy) line closes the window after half a second, so the example also runs without anyone clicking.

7. Exception Handling Interview Questions

An exception is an object that Python raises when an error happens. If no code catches the exception, the program stops and prints a traceback, the list of calls that led to the error.

7.1. How Do try, except, else and finally Work?

The try block holds code that may fail, and the except block handles a matching exception. The else block runs only when no exception happened, whereas the finally block always runs, so we put cleanup there.

def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError as e:
        print("error:", e)
        return None
    else:
        print("no error")
        return result
    finally:
        print("finally runs")

divide(10, 2)
divide(1, 0)
no error
finally runs
error: division by zero
finally runs

For example, a payment call that times out still releases its database connection in the finally block. Interviewers also ask about four more rules.

  • One except can catch several types with a tuple, such as except (ValueError, TypeError) as e:. Python 3.14 also accepts the form without parentheses when there is no as clause.
  • Order matters. Python runs the first matching except block, so specific exceptions go before general ones.
  • A bare except: also catches KeyboardInterrupt and SystemExit, so we catch Exception instead.
  • A return, break or continue inside finally hides exceptions. Python 3.14 emits a SyntaxWarning for these statements in finally (PEP 765).

7.2. How Do You Raise and Create a Custom Exception?

We raise an exception with raise, and we create a custom exception by subclassing Exception. A custom type lets callers catch our specific error, so a checkout page can show a “not enough balance” message for InsufficientFundsError and a generic message for everything else. The form raise … from e links the new exception to the original cause, so the traceback shows both.

class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        super().__init__(f"balance {balance} is less than {amount}")
        self.balance = balance
        self.amount = amount

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

left = withdraw(100, 30)           # 70
failed = withdraw(100, 500)        # InsufficientFundsError: balance 100 is less than 500
def parse_age(text):
    try:
        return int(text)
    except ValueError as e:
        raise RuntimeError("bad age") from e

try:
    parse_age("ten")
except RuntimeError as e:
    print(e, "| caused by:", repr(e.__cause__))
bad age | caused by: ValueError("invalid literal for int() with base 10: 'ten'")

7.3. What Are Exception Groups and except*?

An ExceptionGroup (Python 3.11 and later) carries several exceptions at once, and except* handles each type inside the group separately. Exception groups come from code that runs several tasks together, such as asyncio.TaskGroup, where more than one task can fail.

try:
    raise ExceptionGroup("batch failed", [ValueError("bad price"), TypeError("bad id")])
except* ValueError as eg:
    print("values:", eg.exceptions)
except* TypeError as eg:
    print("types:", eg.exceptions)
values: (ValueError('bad price'),)
types: (TypeError('bad id'),)

With plain except, only one block runs, whereas with except*, more than one block can run for one group.

8. Advanced Python Interview Questions

Experienced candidates get questions about memory and concurrency (doing several tasks at once). Interviewers also ask about the features that make Python code short, such as decorators and generators.

8.1. How Is Memory Managed in Python?

CPython frees unused objects for us, mostly by reference counting. Every object counts the references that point to it, and when the count drops to zero, CPython frees the object at once. Two objects that refer to each other never reach zero, though, so a cyclic garbage collector in the gc module looks for unreachable cycles and frees them.

Python stores objects in a private heap, a memory area managed by the Python memory manager, which uses its own allocator for small objects.

import sys, gc
data = [1, 2, 3]
alias = data
refs = sys.getrefcount(data)       # 3

class Node:
    pass

a, b = Node(), Node()
a.other, b.other = b, a
del a, b
collected = gc.collect()           # 2

The getrefcount() function reports one extra reference, because the function’s own argument also points to the list. The call gc.collect() returned 2, because the collector found the two Node objects of the cycle.

8.2. Why Isn’t All Memory Freed When Python Exits?

At shutdown, CPython does not free every object one by one, because that would slow down every exit. It may leave some objects in place, such as objects in reference cycles or memory held by C extensions, and the operating system frees all memory of the process anyway.

So we do not rely on __del__ for cleanup at exit. Instead, we close files and connections with with blocks or register a cleanup function with the atexit module.

8.3. What Is the GIL and What Is Free-Threaded Python?

The Global Interpreter Lock (GIL) is a lock in CPython that lets only one thread run Python bytecode at a time. It keeps reference counts correct when several threads run, without a lock on every object. The cost is that threads do not run CPU-bound Python code (code that spends its time computing, not waiting) in parallel on several cores.

For example, a script that parses millions of log lines in pure Python gets no speedup from threads on the standard build. Threads still help with I/O, because a thread releases the GIL while it waits for the network or the disk. NumPy also releases the GIL in many operations.

Since Python 3.13, CPython also comes as an optional free-threaded build without the GIL (PEP 703), and Python 3.14 made that build officially supported (PEP 779). The free-threaded interpreter is a separate program, such as python3.14t, while the standard python3.14 still has the GIL.

Single-threaded code runs somewhat slower on the free-threaded build. C extensions must also declare that they support the build, otherwise Python turns the GIL back on.

import sys, sysconfig
gil_disabled = sysconfig.get_config_var("Py_GIL_DISABLED")   # 0
gil_enabled = sys._is_gil_enabled()                          # True
import sys, sysconfig
gil_disabled = sysconfig.get_config_var("Py_GIL_DISABLED")   # 1
gil_enabled = sys._is_gil_enabled()                          # False

The difference shows when we run the same CPU-bound job in 2 threads on a 2-core machine.

import time
from concurrent.futures import ThreadPoolExecutor

def count(n):
    total = 0
    for i in range(n):
        total += i
    return total

start = time.perf_counter()
with ThreadPoolExecutor(max_workers=2) as pool:
    list(pool.map(count, [10_000_000] * 2))
print(f"{time.perf_counter() - start:.2f}s")
python3.14 cpu_threads.py          # 0.54s  (GIL: the threads take turns)
python3.14t cpu_threads.py         # 0.25s  (no GIL: the threads run in parallel)

The timings vary between runs and machines, but the free-threaded build finishes the job in about half the time.

8.4. When Should We Use Threads, Processes or asyncio?

We choose by the kind of work. For work that mostly waits on I/O, such as network calls, we use threads or asyncio, and for CPU-heavy work on the standard build, we use processes.

ToolModuleRuns in parallel on several cores?Good for
Threadsthreading, ThreadPoolExecutorNo with the GIL. Yes on the free-threaded buildNetwork calls, file I/O, a few hundred tasks
Processesmultiprocessing, ProcessPoolExecutorYes. Each process has its own interpreter and GILCPU-heavy work. Data passes between processes by pickling
asyncioasyncioNo. One thread switches between tasks at each awaitThousands of network connections open at the same time
Sub-interpretersconcurrent.interpreters (3.14)Yes. Each interpreter has its own GILCPU work in one process, with separate state for each interpreter
import time
from concurrent.futures import ThreadPoolExecutor

def download(n):
    time.sleep (0.2)
    return n * 10

start = time.perf_counter()
with ThreadPoolExecutor(max_workers=4) as pool:
    results = list(pool.map(download, [1, 2, 3, 4]))   # results = [10, 20, 30, 40]

elapsed = round(time.perf_counter() - start, 1)        # 0.2

Four downloads of 0.2 seconds each took 0.2 seconds in total, because the threads waited at the same time. For example, a page that calls 4 partner APIs waits for the slowest call only, not for the sum of all four. The space in time.sleep (0.2) is valid Python and changes nothing.

8.5. What Are async and await in Python?

The async def statement defines a coroutine, a function that can pause, and await pauses the coroutine until another task finishes. While the coroutine waits, the event loop (the part of asyncio that switches between tasks) runs other tasks.

In asyncio, the run() function starts the event loop, and gather() runs several coroutines at the same time.

import asyncio
import time

async def fetch(name, delay):
    await asyncio.sleep (delay)
    return f"{name} done"

async def main():
    return await asyncio.gather(fetch("a", 0.2), fetch("b", 0.1))

start = time.perf_counter()
results = asyncio.run(main())                     # ['a done', 'b done']
elapsed = round(time.perf_counter() - start, 1)   # 0.2

A blocking call such as time.sleep inside a coroutine stops the whole event loop. So inside async code, we use the asyncio versions or a library such as httpx.

8.6. What Are Decorators in Python?

A decorator adds behavior to a function without changing the function’s code. The decorator is itself a function that takes our function and returns a new function that wraps it, and the wrapper runs extra code before or after the call.

The @decorator line above a def is short for func = decorator(func). We use decorators for logging, timing, caching (functools.cache) and access checks. Frameworks such as Flask use them too (@app.route). For example, a team that wants to log how long each of its 30 service methods takes writes one timing decorator instead of 30 copies of the timing code.

import time
from functools import wraps

def time_it(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.perf_counter() - start:.1f}s")
        return result
    return wrapper

@time_it
def slow_add(a, b):
    time.sleep (0.1)
    return a + b

total = slow_add(2, 3)             # 5
name = slow_add.__name__           # 'slow_add'
slow_add took 0.1s

The @wraps(func) line copies the name and docstring of the original function to the wrapper. Without @wraps, slow_add.__name__ would be ‘wrapper’.

8.7. What Is the Difference Between an Iterator and a Generator?

A generator is a function with yield, and it is the short way to write an iterator. An iterator is any object with __iter__() and __next__() methods, where each next() call returns the following value and, at the end, raises StopIteration.

In a generator, each yield returns one value and pauses the function until the next next() call. Because generators compute values only when asked, they use almost no memory, even for very long or endless sequences. For example, a generator can read a large CSV export row by row and pass each row to the import code, so the whole file never sits in memory.

def fibonacci():
    a, b = 0, 1
    while True:
        yield b
        a, b = b, a + b

fib = fibonacci()
first = next(fib)                  # 1
second = next(fib)                 # 1
third = next(fib)                  # 2
fourth = next(fib)                 # 3
fifth = next(fib)                  # 5
class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

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

values = list(Countdown(3))        # [3, 2, 1]
it = iter([1])
first = next(it)                   # 1
second = next(it)                  # StopIteration:

We can loop over a generator or an iterator only once. Every generator is an iterator, but not every iterator is a generator.

8.8. What Does the with Statement Do?

The with statement sets something up before a block and always cleans up after it, even when the block raises an exception. The object that does the setup and cleanup is called a context manager, and files, locks, database connections and ThreadPoolExecutor are all context managers. A class becomes a context manager by defining __enter__() and __exit__(), and the contextlib.contextmanager decorator makes one from a generator.

from contextlib import contextmanager

@contextmanager
def opened(name):
    print("open", name)
    try:
        yield name.upper()
    finally:
        print("close", name)

with opened("report") as r:
    print("using", r)
open report
using REPORT
close report

The code before yield is the setup, and the finally part is the cleanup. The yielded value goes to the as variable.

8.9. What Is the Difference Between a Shallow Copy and a Deep Copy?

A shallow copy copies only the outer object and shares the nested objects with the original, whereas a deep copy copies the nested objects too, down through every level. The difference shows only with nested mutable objects, such as a list of lists. The methods list.copy() and dict.copy(), the slice lst[:] and the function copy.copy() make shallow copies, whereas copy.deepcopy() makes deep copies.

import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
original[0].append(99)

print(shallow)                             # [[1, 2, 99], [3, 4]]
print(deep)                                # [[1, 2], [3, 4]]
same_list = shallow is original            # False
shared_inner = shallow[0] is original[0]   # True
Shallow copyDeep copy
Outer objectNewNew
Nested objectsShared with the originalCopied
A change to a nested list in the originalVisible in the copyNot visible
Speed and memoryFast, smallSlower, more memory
Howcopy.copy(x), x.copy(), x[:]copy.deepcopy(x)

Plain assignment (b = a) copies nothing, because b is only a second name for the same object. For example, when we copy a default settings dict with nested lists to change one user’s options, a shallow copy also changes the nested lists of the defaults.

8.10. What Are Pickling and Unpickling?

Pickling saves a Python object as bytes, and unpickling turns those bytes back into the object. The general names for these steps are serialization and deserialization. The pickle module handles almost any Python object, including our own classes, and we use it for caches and for sending objects between our own processes.

import pickle
person = {"name": "John", "age": 30, "skills": ["python", "sql"]}

with open("person.pkl", "wb") as f:
    pickle.dump(person, f)

with open("person.pkl", "rb") as f:
    loaded = pickle.load(f)        # loaded = {'name': 'John', 'age': 30, 'skills': ['python', 'sql']}

equal = loaded == person           # True
same = loaded is person            # False

Never unpickle data from an untrusted source, because a pickle can run any code while it loads. For data exchange between systems, we read and write JSON instead.

8.11. What Is New in Recent Python Versions?

Python releases a new version every October. Python 3.14 is the current stable version in October 2026, and Python 3.15 is at the release-candidate stage. Each version gets five years of support.

VersionReleasedFeatures asked about in interviews
3.10Oct 2021match/case pattern matching, X | Y union types, zip(strict=True)
3.11Oct 202210-60% faster CPython, exception groups and except*, tomllib
3.12Oct 2023type statement and generic syntax def f[T](x: T), more flexible f-strings, @override
3.13Oct 2024Experimental free-threaded build and JIT, new interactive shell
3.14Oct 2025Free-threaded build officially supported, t-strings, lazy annotations, concurrent.interpreters, except A, B: without parentheses, compression.zstd

Python 3.10 reached its end of life in October 2026, so new projects start on Python 3.14.

def describe(command):
    match command.split():
        case ["go", direction]:
            return f"going {direction}"
        case ["quit" | "exit"]:
            return "bye"
        case _:
            return "unknown"

north = describe("go north")       # 'going north'
bye = describe("exit")             # 'bye'
other = describe("dance")          # 'unknown'

9. NumPy and Pandas Interview Questions

Data and backend roles often add questions about NumPy and pandas. NumPy gives us fast arrays of numbers, and pandas gives us tables called DataFrames. Both are third-party packages (pip install numpy pandas), and the examples use NumPy 2.5 and pandas 3.0.

9.1. What Is the Difference Between NumPy and SciPy?

NumPy gives us fast arrays and basic math on them, and SciPy builds on NumPy to add scientific algorithms. The NumPy array type is called ndarray, and SciPy depends on NumPy, not the other way round.

NumPySciPy
Corendarray: fast multi-dimensional arraysAlgorithms that work on NumPy arrays
MathElement-wise math, basic linear algebra, random numbers, FFTOptimization, integration, interpolation, signal processing, sparse matrices
StatisticsMean, median, percentilesProbability distributions, hypothesis tests
Typical useAny numeric code. Base of pandas and scikit-learnEngineering and scientific computing

9.2. How Do You Get the Indices of the N Largest Values in a NumPy Array?

We sort the indices with np.argsort() and take the last N. The np.argsort() function returns the indices that would sort the array, from the smallest value to the largest, so the last N indices belong to the N largest values. For a large array, np.argpartition() is faster, because it does not sort the whole array; it only splits the array around the N-th largest value. For example, a shop dashboard that shows the 3 best-selling products of the day needs only their positions in the sales array.

import numpy as np
arr = np.array([10, 5, 8, 20, 9])
n = 3

order = np.argsort(arr)                      # array([1, 2, 4, 0, 3])
top = np.argsort(arr)[-n:]                   # array([4, 0, 3])
top_first = np.argsort(arr)[-n:][::-1]       # array([3, 0, 4])

idx = np.argpartition(arr, -n)[-n:]
top_fast = idx[np.argsort(arr[idx])][::-1]   # array([3, 0, 4])
largest = arr[[3, 0, 4]]                     # array([20, 10,  9])

The argsort() function sorts from small to large, so we reverse the last N indices to list the largest value first. Index 3 holds 20, index 0 holds 10 and index 4 holds 9.

9.3. What Is the Easiest Way to Calculate Percentiles in Python?

We call np.percentile(data, q) with q between 0 and 100. When the percentile falls between two data points, the method argument chooses how to pick a value there, and the default is “linear”. For example, an API team reports the 90th percentile of response times, because the average hides the slow requests.

NumPy 2 removed the old interpolation argument. So interpolation=”nearest” from older answers raises TypeError. Without NumPy, statistics.quantiles() from the standard library does the same job.

import numpy as np
import statistics
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

p90 = np.percentile(data, 90)                                # np.float64(9.1)
p90_nearest = np.percentile(data, 90, method="nearest")      # np.int64(9)
p90_old = np.percentile(data, 90, interpolation="nearest")   # TypeError: percentile() got an unexpected keyword argument 'interpolation'
p90_stdlib = statistics.quantiles(data, n=10)[-1]            # 9.9

The standard library uses a different default method, so for the same data its 90th percentile (9.9) differs from NumPy’s (9.1).

9.4. How Do You Combine Different Pandas DataFrames?

Pandas has three tools for combining DataFrames, and they differ in how they line up the rows.

  • The pd.concat([df1, df2]) function stacks DataFrames on top of each other, or side by side with axis=1.
  • The pd.merge(df1, df2, on=”col”) function joins rows on common columns, like a SQL join. The how argument chooses “inner”, “left”, “right” or “outer”, and left_on and right_on handle columns with different names.
  • The df1.join(df2) method joins on the index, the row labels of the DataFrame.
import pandas as pd
prices = pd.DataFrame({"fruit": ["apple", "banana"], "price": [5, 3]})
more = pd.DataFrame({"fruit": ["cherry"], "price": [7]})
stock = pd.DataFrame({"fruit": ["apple", "cherry"], "qty": [10, 4]})

all_prices = pd.concat([prices, more], ignore_index=True)
print(all_prices)
print(pd.merge(all_prices, stock, on="fruit", how="left"))
    fruit  price
0   apple      5
1  banana      3
2  cherry      7
    fruit  price   qty
0   apple      5  10.0
1  banana      3   NaN
2  cherry      7   4.0

The left join keeps banana, which has no stock row, so pandas fills its qty with NaN, the marker for a missing value. Because NaN is a float, the whole column turned into floats.

9.5. How Do You Get the Items of Series A That Are Not in Series B?

We keep the items of A for which isin(B) is false. The isin() method marks each item of A that also appears in B with True, and the ~ operator flips every True to False and every False to True.

import pandas as pd
series_a = pd.Series([1, 2, 3, 4, 5])
series_b = pd.Series([3, 4, 5, 6, 7])
print(series_a[~series_a.isin(series_b)])
0    1
1    2
dtype: int64

9.6. How Do You Get the Items That Are Not Common to Series A and B?

The items that are in only one of the two Series form the symmetric difference. In pandas, we take the union of both Series and remove the common items, and plain Python sets give the same result with the ^ operator.

import numpy as np
import pandas as pd
series_a = pd.Series([1, 2, 3, 4, 5])
series_b = pd.Series([3, 4, 5, 6, 7])

union = pd.Series(np.union1d(series_a, series_b))
common = np.intersect1d(series_a, series_b)
not_common = union[~union.isin(common)].tolist()             # [1, 2, 6, 7]
sym_diff = set(series_a.tolist()) ^ set(series_b.tolist())   # {1, 2, 6, 7}

We call tolist() before set() because, in NumPy 2, a set of NumPy values prints as np.int64(1) and so on, whereas tolist() turns the values into plain Python ints.

9.7. Can Pandas Recognize Dates While Importing Data?

Yes, pd.read_csv(…, parse_dates=[“col”]) turns a column into dates while reading the file. The pd.to_datetime() function converts strings that are already in a DataFrame, and pd.date_range() generates a sequence of dates.

With date columns, we can filter by date, group by month and resample, which means grouping rows by a time period, such as a week. Without pandas, the csv module reads and writes CSV files but returns every value as a string, dates included.

import pandas as pd
df = pd.read_csv("sales.csv", parse_dates=["day"])
day_type = str(df["day"].dtype)                                # 'datetime64[us]'
day_names = df["day"].dt.day_name().tolist()                   # ['Thursday', 'Friday']
day = pd.to_datetime("04-10-2026", format="%d-%m-%Y")          # Timestamp('2026-10-04 00:00:00')
week = len(pd.date_range("2026-10-01", periods=7, freq="D"))   # 7

9.8. How Do You Read a Publicly Shared CSV File From Google Drive?

We share the file as “Anyone with the link” and read it through its direct download URL. We take the file ID from the share link, build the download URL and pass that URL to pd.read_csv(). The ID is the part between /d/ and /view in a link such as https://drive.google.com/file/d/FILE_ID/view.

def drive_csv_url(share_link):
    file_id = share_link.split("/d/")[1].split("/")[0]
    return f"https://drive.google.com/uc?export=download&id={file_id}"

url = drive_csv_url("https://drive.google.com/file/d/1AbC9xYz/view?usp=sharing")  # 'https://drive.google.com/uc?export=download&id=1AbC9xYz'

Then df = pd.read_csv(drive_csv_url(link)) reads the file like any other CSV URL. For a Google Sheet, the export URL is https://docs.google.com/spreadsheets/d/SHEET_ID/export?format=csv.

10. Python Coding Interview Questions

Coding rounds test whether we can turn a small problem into clean code, and interviewers also ask how much time and memory the code needs. For each problem, we give a readable solution first and then mention the built-in shortcut, because interviewers often ask for both.

10.1. Write a Program to Produce the Fibonacci Series

In the Fibonacci series, each number is the sum of the two numbers before it, starting with 0 and 1. A loop with two variables builds the series in O(n) time, which means that the work grows in step with n.

def fibonacci(n):
    a, b = 0, 1
    result = []
    for _ in range(n):
        result.append(a)
        a, b = b, a + b
    return result

ten = fibonacci(10)                # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
one = fibonacci(1)                 # [0]
empty = fibonacci(0)               # []

Interviewers often ask for the recursive version next. Without a cache, the recursive version computes the same values again and again, so the work grows exponentially. The @functools.cache decorator removes the repeated work by storing each result once.

from functools import cache

@cache
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

fib_10 = fib(10)                   # 55
fib_50 = fib(50)                   # 12586269025

10.2. Write a Program to Check if a Number Is Prime

A prime is a whole number greater than 1 that only 1 and itself divide evenly. We test divisors only up to the square root of the number, because a larger divisor always pairs with a smaller one that we have already tested. The math.isqrt() function returns that square root as an exact integer.

import math

def is_prime(num):
    if num < 2:
        return False
    if num % 2 == 0:
        return num == 2
    for i in range(3, math.isqrt(num) + 1, 2):
        if num % i == 0:
            return False
    return True

primes = [n for n in range(20) if is_prime(n)]   # [2, 3, 5, 7, 11, 13, 17, 19]
prime_97 = is_prime(97)                          # True
prime_91 = is_prime(91)                          # False

10.3. Write a Program to Check if a Sequence Is a Palindrome

A palindrome reads the same backward and forward. We lowercase the text, keep only letters and digits with str.isalnum, and compare the result with its reverse, [::-1].

def is_palindrome(text):
    cleaned = "".join(filter(str.isalnum, text.lower()))
    return cleaned == cleaned[::-1]

sentence = is_palindrome("A man, a plan, a canal: Panama")   # True
word = is_palindrome("hello")                                # False
seq = [1, 2, 1]
seq_palindrome = seq == seq[::-1]                            # True

For a list, the same comparison seq == seq[::-1] works without the cleaning step, as the last line shows.

10.4. Count the Capital Letters in a File That Does Not Fit in Memory

We read the file in chunks of a fixed size, so only one chunk is in memory at a time. The str.isupper() method tells us whether a character is a capital letter, and it also counts accented capitals, such as the “E” with an acute accent.

def count_caps(path, chunk_size=1024 * 1024):
    count = 0
    with open(path, encoding="utf-8") as f:
        while chunk := f.read(chunk_size):
            count += sum(1 for ch in chunk if ch.isupper())
    return count

caps = count_caps("big.txt")       # 15

The test file big.txt holds the lines “Hello World” and “Python Is Fun” three times, and each pair of lines has 5 capitals.

10.5. Write a Sorting Algorithm for a Numerical Dataset

In real code, we call sorted(nums). In an interview, we may have to write a sort by hand, and bubble sort is the shortest to explain.

Bubble sort swaps neighbors that are in the wrong order, so each pass moves the largest remaining value to the end, and it stops when a full pass makes no swap. Bubble sort takes O(n^2) time, while sorted() takes O(n log n).

def bubble_sort(nums):
    arr = list(nums)
    n = len(arr)
    for i in range(n):
        swapped = False
        for j in range(n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                swapped = True
        if not swapped:
            break
    return arr

by_hand = bubble_sort([5, 2, 9, 1, 5])   # [1, 2, 5, 5, 9]
built_in = sorted([5, 2, 9, 1, 5])       # [1, 2, 5, 5, 9]

10.6. Convert a Date From yyyy-mm-dd to dd-mm-yyyy Format

We read (parse) the date with datetime.strptime() and write it in the new format with strftime(). Parsing also checks the date, so an impossible date, such as February 30, raises ValueError.

from datetime import datetime

def convert_date(date_str):
    return datetime.strptime(date_str, "%Y-%m-%d").strftime("%d-%m-%Y")

converted = convert_date("2026-10-04")   # '04-10-2026'
invalid = convert_date("2026-02-30")     # ValueError: day 30 must be in range 1..28 for month 2 in year 2026

10.7. Match a String That Has an a Followed by 4 to 8 b’s

The pattern ab{4,8} means “an a, then 4 to 8 b characters”. The re.fullmatch() function requires the whole string to match the pattern. With re.search() instead, “abbbbbbbbb” (nine b’s) would also match, because re.search() finds an a with eight b’s inside the string and ignores the rest.

import re

def match_string(text):
    return re.fullmatch(r"ab{4,8}", text) is not None

four_bs = match_string("abbbb")        # True
three_bs = match_string("abbb")        # False
nine_bs = match_string("abbbbbbbbb")   # False
prefixed = match_string("xabbbb")      # False

10.8. Check if All Numbers in a Sequence Are Unique

A set drops duplicates, so the numbers are unique when the set has the same length as the sequence. The loop version stops at the first duplicate, which saves work for long sequences.

def is_unique(seq):
    seen = set()
    for num in seq:
        if num in seen:
            return False
        seen.add(num)
    return True

unique = is_unique([1, 2, 3, 4])       # True
repeated = is_unique([1, 2, 3, 2])     # False
by_set = len(set([1, 2, 3, 2])) == 4   # False

To drop the duplicates instead, we remove duplicates and keep the original order.

10.9. Count the Occurrences of Every Character in a Text File

The collections.Counter class counts how often each item of an iterable appears. A file object gives us one line at a time, and a line is an iterable of characters, so we pass each line to Counter. Counter is a dict subclass, and most_common(n) returns the most frequent items.

from collections import Counter

def count_chars(path):
    counts = Counter()
    with open(path, encoding="utf-8") as f:
        for line in f:
            counts.update(line)
    return counts

counts = count_chars("notes.txt")
top_three = counts.most_common(3)  # [('l', 4), (' ', 2), ('\n', 2)]
capital_b = counts["B"]            # 1
letter_z = counts["z"]             # 0

The file notes.txt contains “Buy milk” and “Call Alex”. A missing key returns 0 instead of raising KeyError.

10.10. Find All Pairs in an Array Whose Sum Equals a Target

The nested-loop solution checks every pair, which takes O(n^2) time. The set solution remembers the numbers seen so far and, for each number, checks whether the missing partner is already in the set, so it finds all pairs in one pass, in O(n) time.

def find_pairs_brute(nums, target):
    pairs = []
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] + nums[j] == target:
                pairs.append((nums[i], nums[j]))
    return pairs

def find_pairs(nums, target):
    seen, pairs = set(), []
    for n in nums:
        if target - n in seen:
            pairs.append((target - n, n))
        seen.add(n)
    return pairs

slow_pairs = find_pairs_brute([2, 7, 4, 5, 3], 9)   # [(2, 7), (4, 5)]
fast_pairs = find_pairs([2, 7, 4, 5, 3], 9)         # [(2, 7), (4, 5)]

10.11. Calculate the Sum of a List of Numbers

The built-in sum() adds all items of a list. The loop version shows the same logic by hand, but for floats the two give different results. Since Python 3.12, sum() uses compensated summation, a method that cancels most rounding errors, which a plain loop does not do. The math.fsum() function gives the correctly rounded result.

import math

def sum_list(numbers):
    total = 0
    for num in numbers:
        total += num
    return total

loop_total = sum_list([1, 2, 3, 4])    # 10
builtin_total = sum([1, 2, 3, 4])      # 10
loop_floats = sum_list([0.1] * 10)     # 0.9999999999999999
builtin_floats = sum([0.1] * 10)       # 1.0
exact_floats = math.fsum([0.1] * 10)   # 1.0

10.12. Merge Two Dictionaries and Sum the Values of Common Keys

We copy the first dict and then add each value of the second dict, and get(key, 0) returns 0 for keys that are not there yet. The | operator (Python 3.9 and later) merges two dicts too, but for a common key it does not add the values. Instead, the value from the right side replaces the left one.

def merge_sum(d1, d2):
    result = dict(d1)
    for key, value in d2.items():
        result[key] = result.get(key, 0) + value
    return result

d1 = {"apple": 5, "banana": 3}
d2 = {"banana": 2, "cherry": 7}
summed = merge_sum(d1, d2)         # {'apple': 5, 'banana': 5, 'cherry': 7}
merged = d1 | d2                   # {'apple': 5, 'banana': 2, 'cherry': 7}

10.13. Add Two Positive Integers Without Using the + Operator

We add the numbers bit by bit, with XOR for the sum and AND for the carry. XOR (^) adds the bits without the carry, and AND (&), shifted left by one place (<< 1), gives the carry. We repeat until no carry is left.

def add(a, b):
    while b != 0:
        carry = (a & b) << 1
        a = a ^ b
        b = carry
    return a

total1 = add(5, 7)                 # 12
total2 = add(1, 0)                 # 1
total3 = add(255, 1)               # 256

Take 5 (101) and 7 (111). XOR gives 010, the carry is 1010, and the loop continues until the carry is 0.

The loop works for positive numbers only, because Python integers have no fixed width, so a negative number never runs out of carry bits.

The call sum((a, b)) also avoids the + sign, but sum() adds with + internally, so most interviewers do not accept that answer.

10.14. Print the Table of 2 Using a while Loop

A while loop runs as long as its condition is true. So we must increase the counter inside the loop ourselves, otherwise the loop never ends.

i = 1
while i <= 10:
    print(f"2 x {i} = {2 * i}")
    i += 1
2 x 1 = 2
2 x 2 = 4
2 x 3 = 6
2 x 4 = 8
2 x 5 = 10
2 x 6 = 12
2 x 7 = 14
2 x 8 = 16
2 x 9 = 18
2 x 10 = 20

10.15. Write a Function That Prints Whether a Value Is Even or Odd

A number is even when num % 2 is 0. The modulo operator % returns the remainder of a division, and in Python it also works for negative numbers, because the result takes the sign of the divisor.

def even_or_odd(num):
    print(f"{num} is {'even' if num % 2 == 0 else 'odd'}")

even_or_odd(4)
even_or_odd(7)
even_or_odd(-3)
4 is even
7 is odd
-3 is odd

10.16. Find the Minimum and Maximum Values in a Tuple

The built-in min() and max() functions work on any iterable, including tuples. Both accept a key function, such as len, and a default value for empty input.

tup = (3, 5, 1, 9, 2)
smallest = min(tup)                                 # 1
largest = max(tup)                                  # 9
longest = max(("kiwi", "banana", "fig"), key=len)   # 'banana'
fallback = min((), default=0)                       # 0

10.17. How Do You Print a Star Pattern Without a Newline or Space?

We pass end=”” and sep=”” to print(). By default, print() ends with a newline and puts a space between its arguments, so end=”” turns off the newline and sep=”” turns off the space. The first loop prints 5 rows of 5 stars as one line, because every print() call ends without a newline.

for i in range(5):
    for j in range(5):
        print("*", end="")
print()
*************************
for i in range(1, 5):
    print("*" * i)
*
**
***
****

11. Python Interview FAQs

Candidates also ask how to prepare, how much Python a job needs and whether Java or Python is the better choice.

11.1. How Should I Prepare for a Python Interview?

We prepare best in a fixed order, from the basics to a project.

  1. Learn the basics until we can answer the top 10 questions at the start of this page without notes.
  2. Write the coding problems from section 10 from memory, then solve similar problems on a practice site.
  3. Read the job description. A data role adds NumPy and pandas, a backend role adds Django, Flask or FastAPI, and almost every role adds SQL and Git.
  4. Build one small project from start to finish, and be ready to explain why we designed it the way we did.
  5. Name the trade-off in each answer. For example, “a set lookup is O(1) on average, and a list lookup is O(n)“.

11.2. How Much Python Do I Need to Get a Job?

For a junior role, we need the topics of sections 1 to 7, such as data types, functions, classes, modules, exceptions and file handling. Interviewers also expect pip, virtual environments, Git and one framework or library for the target field, whereas generators, decorators, the GIL and concurrency (section 8) separate mid-level candidates from beginners.

11.3. Which Is Better, Java or Python?

Neither is better in general, because each language fits different projects. Many teams use both, with Java for the core services and Python for data work and automation.

JavaPython
TypingStatic, checked by the compilerDynamic, optional type hints
SpeedFaster for CPU-heavy code (JIT)Slower for pure Python loops. Fast with NumPy and C extensions
ConcurrencyReal parallel threads, virtual threadsGIL in the standard build. Free-threaded build since 3.13
Strong areasLarge enterprise systems, Android, high-throughput servicesData science, machine learning, scripting, automation, web APIs
Code lengthLongerShorter

12. Conclusion

Most Python interview questions test a few core rules.

  • Which types are mutable.
  • How names refer to objects.
  • How scope works.
  • How Python runs code and frees memory.

If we can explain the mutability, naming, scope and memory rules and show each one in a three-line snippet, we can answer most variants of these questions. For deeper study, we work through the official Python tutorial and the other interview guides on this site.

13. References

The official Python documentation and PEPs cover each rule in more depth.

Happy Learning !!

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.