Python for AI/ML · 10 questions
Core Python
Q1. What are Python's main built-in data types?
Immutable:
int,float,bool,str,tuple,bytes.Mutable:
list,dict,set,bytearray.mutable objects can change their value or state after creation, while immutable objects cannot be modified at all
Q2. What is mutability and why does it matter?
Mutability determines whether an object's value can be changed in memory after it is created. If an object is mutable, it can be updated in place. If it is immutable, it can't be updated in place.
This distinction matters because it impacts how your code runs, how much memory it uses, and whether it introduces hard-to-find bugs.
Preventing Accidental Data Corruption (Shared References): In Python, multiple variables can point to the exact same object in memory. If that object is mutable, changing it via one variable silently alters it for the other.
# The Danger with Mutable Objects
original_prices = [10, 20, 30]
sale_prices = original_prices # Both point to the same list
sale_prices[0] = 5 # Modifying the copy...
print(original_prices) # Output: [5, 20, 30] -> The original data is ruined!The Danger of Default Arguments in Functions: A classic Python trap occurs when you use a mutable object (like an empty list) as a default argument in a function definition. Python evaluates that default list only once when the function is defined, not every time it runs.
def add_to_team(name, team=[]): # 'team' is created once and reused
team.append(name)
return team
print(add_to_team("Alice")) # Output: ['Alice']
print(add_to_team("Bob")) # Output: ['Alice', 'Bob'] -> Unexpected! Bob hijacked Alice's team.
Memory and Performance Trade-offs: Because immutable objects cannot change, Python optimizes them behind the scenes.
Immutable benefits: Reusing small integers or short strings saves memory. They are also faster to copy since Python only needs to copy the memory address reference, not the whole object data.
Mutable benefits: Updating a massive list with millions of items is incredibly fast because Python updates it in place. If that list were immutable, adding a single item would force Python to copy all millions of items into a brand-new memory location, slowing your program to a crawl.
Q3. is vs ==?
==compares values;iscompares identity (same object in memory). Useisonly forNone,True,False.
x = [1, 2]; y = [1, 2]
x == y # True
x is y # FalseQ4. Shallow copy vs deep copy?
A shallow copy creates a new object and copies the top-level properties or references, meaning any nested objects or arrays are still shared between the original and the copy.
A deep copy creates a completely independent duplicate of the entire object graph, recursively copying all nested structures so that changes never affect the original.
import copy
old = [[1, 2], [3]]
shallow = copy.copy(old)
deep = copy.deepcopy(old)
old[0].append(99)
print(shallow[0]) # [1, 2, 99]
print(deep[0]) # [1, 2]Q5. List vs tuple vs set vs dict?
Type | Ordered | Mutable | Duplicates | Typical use |
|---|---|---|---|---|
list | yes | yes | yes | sequences |
tuple | yes | no | yes | fixed records, dict keys |
set | no | yes | no | membership, dedupe |
dict | yes (3.7+) | yes | unique keys | key-value lookup |
Q6. What is a list comprehension? Give dict and set versions.
A list comprehension provides a concise way to create lists in Python. It replaces bulky
forloops and.append()statements with a single, highly readable line of code.
# Instead of setting up an empty list, looping, and filtering, you write the logic directly inside brackets: new_list = [expression for item in iterable if condition]
squares = [x * x for x in range(5) if x % 2 == 0] # [0, 4, 16]
lengths = {w: len(w) for w in ["ai", "llm"]} # {'ai': 2, 'llm': 3}
unique = {x % 3 for x in range(10)} # {0, 1, 2}A dictionary comprehension uses curly braces
{}and requires akey: valuepair format separated by a colon.
numbers = [1, 2, 3, 4]
cube_dict = {x: x ** 3 for x in numbers}
# Output: {1: 1, 2: 8, 3: 27, 4: 64}
# Using a condition (only mapping even numbers)
even_cubes = {x: x ** 3 for x in numbers if x % 2 == 0}
# Output: {2: 8, 4: 64}
A set comprehension also uses curly braces
{}, but it only specifies a single value (just like a list comprehension). Because it creates a set, it will automatically remove duplicates and ignore order.
# Extracting unique lengths of words, ignoring case
words = ["Python", "java", "Go", "JAVA", "python"]
# Notice the duplicates will be filtered out automatically
word_lengths = {len(w.lower()) for w in words}
print(word_lengths)
# Output: {2, 4, 6}
Q7. How do you remove duplicates from a list while keeping order?
The most efficient way to remove duplicates from a list while keeping the original order is to use
dict.fromkeys().Because Python dictionaries preserve insertion order, converting a list into dictionary keys instantly drops duplicate values while keeping everything else exactly in place.
items = [3, 1, 3, 2, 1]
unique = list(dict.fromkeys(items)) # [3, 1, 2]Q8. What is the mutable default argument trap?
The mutable default argument trap occurs when you use a mutable object like a list
[], a dictionary{}, or a set as a default parameter in a function definition.In Python, default arguments are evaluated exactly once when the function is first defined, not every time the function is called. This means that if you modify that mutable default argument inside the function, you are changing a single, shared object that persists across every subsequent call to that function.
def append_to_list(element, target_list=[]): # The trap is set here
target_list.append(element)
return target_list
# First call works as expected
print(append_to_list("apple"))
# Output: ['apple']
# Second call targets a brand new item... or does it?
print(append_to_list("banana"))
# Output: ['apple', 'banana'] -> Uh oh! 'apple' is still there.
# Third call continues to pollute the same list
print(append_to_list("cherry"))
# Output: ['apple', 'banana', 'cherry']
# How to Fix It: The Idiomatic Python Solution
# To avoid this trap, use None as the default placeholder value. None is immutable, making it completely safe. Then, check for None inside the function body and instantiate a fresh, brand-new mutable object on the spot.
def append_to_list(element, target_list = None): # Safe immutable placeholder
if target_list is None:
target_list = [] # A brand-new list is created every single execution
target_list.append(element)
return target_list
print(append_to_list("apple")) # Output: ['apple']
print(append_to_list("banana")) # Output: ['banana']
print(append_to_list("cherry")) # Output: ['cherry']
Q9. When is a mutable default argument actually useful?
The trap is only a trap if it's accidental. Python developers sometimes intentionally exploit this unique memory behavior to build memoization systems (caches).
Because a mutable dictionary persists across function executions, you can pass an empty dict as a default parameter to cache expensive calculations directly inside the function without needing global variables.
Q10. How does dict.get() differ from dict[key]?
The core difference between
dict.get(key)anddict[key]lies in how they handle missing keys.When a key does not exist in a dictionary,
dict[key]will immediately crash your program by raising aKeyError, whereasdict.get(key)will gracefully return a fallback value (by default,None)without giving an error.
Q11. What does zip, enumerate, and sorted(key=...) do?
These three built-in Python tools are essential for looping, tracking and organizing data efficiently without writing bulky manual loops.
Function | What it returns / does | Best Used For |
|---|---|---|
| It pairs the 1st items of two lists together, then the 2nd items, then the 3rd items, and stops when the shortest list runs out. | Looping over two or more lists at the exact same time. |
| The | Keeping track of the loop counter / index automatically. |
| The | Sorting complex data (like dicts or tuples) by specific fields. |
names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
# Loop through both lists simultaneously
for name, score in zip(names, scores):
print(f"{name} scored {score}")
# Alice scored 85
# Bob scored 92
# Charlie scored 78tasks = ["Write code", "Test app", "Deploy to production"]
# Get the counter and the item at the same time
for index, task in enumerate(tasks, start=1): # Can change start from default 0 to 1
print(f"Task {index}: {task}")
# Task 1: Write code
# Task 2: Test app
# Task 3: Deploy to production
fruits = ["banana", "strawberry", "apple"]
# Sort items based on the result of the len() function
ordered_fruits = sorted(fruits, key=len)
print(ordered_fruits)
# Output: ['apple', 'banana', 'strawberry']Q12. How are strings formatted? What are f-strings?
There are three primary ways to handle string formatting, with f-strings being the modern, industry-standard approach.
An f-string (short for formatted string literal) is a string prefixed with the letter
forF.It allows you to embed Python expressions directly inside curly braces
{}.
Method | Syntax Example | Notes |
|---|---|---|
|
| Oldest method (C-style); hard to read with multiple variables. |
|
| Introduced in Python 2.6; better, but can become verbose. |
|
| Modern Standard (Python 3.6+); cleanest, fastest and most powerful. |
# Ex1:
name, score = "Ritesh", 0.9234
print(f"{name}: {score:.2%}") # Ritesh: 92.34%
# Ex2:
name = "Alice"
age = 30
# Elegant, clean inline variables
message = f"My name is {name} and I am {age} years old."
print(message) #My name is Alice and I am 30 years old.
print(f"Next year I will be {age + 1}.") # Output: Next year I will be 23.
print(f"Shouting my name: {name.upper()}") # Output: Shouting my name: RITESHQ13. What is the GIL? Why does Python have a GIL?
The GIL (Global Interpreter Lock) is a mechanism in the standard Python interpreter (CPython) that ensures only one thread executes Python bytecode at a time, even if your computer has a modern CPU with 4, 8 or 16 cores.
The GIL exists because of how CPython manages memory.
CPython uses a system called reference counting to keep track of objects. When you create a variable, its reference count goes up, when you delete it, the count goes down. If the count hits zero, Python safely frees that memory.
If multiple threads were allowed to run simultaneously, they could modify these reference counts at the exact same time. This would lead to a catastrophic race condition, resulting in memory leaks or corrupting data and crashing your application.
To prevent this, the creators of Python chose a simple solution: lock the entire interpreter so that only one thread can run at a fraction of a second.
How to Bypass the GIL
If you need true, multi-core parallel processing in Python for heavy tasks, you have below primary workarounds:
Use the
multiprocessingmodule: Create entirely separate Python processes. Each process gets its own distinct Python interpreter and its own GIL, allowing your code to scale across all CPU cores.Leverage C-extensions (NumPy/Pandas): Heavy data libraries like NumPy do their calculations in underlying C code. They release the GIL before starting massive matrix math, allowing true parallel operations outside of Python's main loop.
Q14. What does if __name__ == "__main__": do?
name== "__main__":statement in Python is a conditional check used to determine whether the script is being run directly as the main program or imported as a module into another script.It allows a single Python file to serve two purposes: it can act as a standalone, runnable script and it can also be a reusable library of functions for other files without causing side effects.
How it Works Behind the Scenes
Every time Python runs a file, it automatically defines a collection of special variables behind the scenes. One of these variables is
name.The value assigned to
namechanges based on how you run the file.If you run the file directly (e.g., typing
python script.pyin your terminal), Python setsnameto the string"__main__".If you import the file inside another script (e.g., typing
import scriptinsidemain.py), Python setsnameto the actual filename (e.g.,"script")
# math_helper.py
def calculate_square(x):
return x * x
# This code block only triggers if math_helper.py is run DIRECTLY
if __name__ == "__main__":
print("Testing math_helper directly...")
result = calculate_square(4)
print(f"The square of 4 is {result}")
# Scenario A: Running the file directly
# If you execute python math_helper.py in your terminal, Python sets __name__ = "__main__". The if statement evaluates to True, and your console outputs:
# Testing math_helper directly...
# The square of 4 is 16
# Scenario B: Importing the file into another script
# Now, imagine you create a second file called app.py and import your helper:
# app.py
import math_helper
print("App is running!")
print(math_helper.calculate_square(5))
# When you run python app.py, Python loads math_helper.py. However, because it is an import, Python sets __name__ = "math_helper" inside that file. The if block evaluates to False and is completely ignored.
# Your console outputs only what matters to app.py:
App is running!
25
# If the if __name__ == "__main__": guard wasn't there, importing the module would have accidentally printed out the "Testing math_helper..." block right in the middle of your main application!Q15. * unpacking and walrus operator?
Both asterisk unpacking (
*) and the walrus operator (:=) are powerful Python features used to write clean, concise and efficient code.Unpacking means assigning elements of an iterable (list, tuple, string, etc.) to multiple variables in a single statement.
# Basic unpacking
numbers = [10, 20, 30]
a, b, c = numbers
print(a) # 10
print(b) # 20
print(c) # 30
# Using * for extended unpacking: You can use * to capture multiple elements into a list when unpacking a collection
# Grabbing first, last, and everything in between
first, *middle, last = [1, 2, 3, 4, 5]
print(first) # Output: 1
print(middle) # Output: [2, 3, 4] (always unpacks into a list)
print(last) # Output: 5
# Swapping variables
a = 10
b = 20
a, b = b, a
print(a, b) # 20 10**is called double-asterisk unpacking. It is mainly used to unpack dictionaries into keyword arguments or merge dictionaries.
# Merging dictionaries
a = {"name": "Ritesh"}
b = {"age": 21}
c = {**a, **b}
print(c) # {'name': 'Ritesh', 'age': 21}Operator | Purpose |
|---|---|
| Unpacks iterables into positional arguments |
| Unpacks dictionaries into keyword arguments |
Functions, decorators, generators
Q16. What are *args and **kwargs?
*argsand**kwargsare special syntaxes that allow a function to accept a variable number of arguments.You use them when you don't know, how many inputs a user might pass into the function
*args(Positional Arguments Bucket): Collects any number of extra positional arguments into a tuple.**kwargs(Keyword Arguments Bucket): Collects any number of extra keyword (named) arguments into a dictionary (where keys are argument names and values are the values).
# Ex1:
def multiply_numbers(*args):
# 'args' inside the function is a tuple: (2, 3, 4)
result = 1
for num in args:
result *= num
return result
print(multiply_numbers(2, 3, 4)) # Output: 24
# Ex2:
def print_details(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_details(name="Alice", age=25)
# Output:
# name: Alice
# age: 25
Q17. What is a closure?
A closure in Python is a nested function that remembers and has access to variables from its outer (enclosing) function's scope, even after the outer function has finished executing.
The Three Conditions for a Closure: For a closure to exist, three specific criteria must be met:
There must be a nested function (a function inside a function).
The nested function must reference a value belonging to the outer function.
The outer function must return the nested function.
def make_multiplier(x):
# The outer function takes 'x'
def multiplier(y):
# The inner function references 'x' from the outer scope
return x * y
# Returns the inner function object
return multiplier
# Create two custom functions
times_two = make_multiplier(2)
times_three = make_multiplier(3)
# Use the closures
print(times_two(10)) # Output: 20
print(times_three(10)) # Output: 30
Q18. What is a decorator?
A decorator in Python is a function that takes another function as an argument, extends or modifies its behavior and returns a new function, all without permanently changing the source code of the original function.
Decorators are built directly on top of closures and
*args/**kwargs. You typically apply them using the@decorator_namesyntax right above a function definition.
# 1. Define the decorator
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func() # This runs the original function
print("Something is happening after the function is called.")
return wrapper
# 2. Apply the decorator
@my_decorator
def say_hello():
print("Hello!")
# 3. Call the function
say_hello()
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.Q19. Write a decorator that takes arguments.
To write a decorator that takes arguments in Python, you need to add an extra outer layer function. This creates a three-level nested function structure (a decorator factory) where:
The outermost function accepts the custom decorator arguments.
The middle function receives the actual function to be decorated.
The innermost function handles the arguments passed to the decorated function itself.
import time
from functools import wraps
def retry(times=3, delay=1.0):
def deco(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return fn(*args, **kwargs)
except Exception:
if attempt == times:
raise
time.sleep(delay * attempt)
return wrapper
return decoQ20. What is a generator and why use one?
A generator in Python is a special type of function that returns an iterable sequence of values - meaning it produces items one at a time, only when requested, rather than computing them all at once and saving them in memory.
Instead of using a
returnstatement, generators use theyieldkeyword. When the generator function is called, it does not actually run the code; it returns a generator object.Every time we call
next()on it, it runs until it hitsyield, pauses right there, remembers its entire state, and outputs the value.Feature
Normal Function (return)
Generator Function (yield)
Execution
Runs completely from start to finish.
Pauses and resumes its state at every
yield.Memory Output
Creates the entire list in memory instantly.
Generates one item at a time on the fly.
# A normal function creates the whole list in memory
def get_numbers_list(n):
result = []
for i in range(n):
result.append(i)
return result
# A generator yields one item at a time
def get_numbers_gen(n):
for i in range(n):
yield iQ21. Iterator vs iterable?
An iterable is something you can loop over, while an iterator is the actual agent that performs the looping and remembers its position.
An iterable has
__iter__( Lists, Tuples, Strings, Dictionaries, Sets )An iterator has
__next__and keeps position.Generators are iterators.
When you write a standard
for item in my_list:loop, Python automatically converts your iterable into an iterator behind the scenes.You cannot call
next()directly on a list or string, because an iterable is not an iterator. To fix this, you must always pass it toiter()first to get a stateful tracker.
numbers = [1, 2, 3]
next(numbers) # TypeError: 'list' object is not an iterator
# 1. This is an ITERABLE (a standard list)
my_iterable = [10, 20, 30]
# 2. Convert it to an ITERATOR using iter()
# This triggers the __iter__() method under the hood
my_iterator = iter(my_iterable)
# 3. Fetch items one by one using next(). This triggers the __next__() method under the hood
print(next(my_iterator)) # Output: 10
print(next(my_iterator)) # Output: 20
print(next(my_iterator)) # Output: 30
# 4. Once empty, it raises a StopIteration exception
# This tells a 'for' loop to stop gracefully
# print(next(my_iterator)) -> Raises StopIteration
Q22. lambda, map, filter: when to use them?
They are designed to manipulate collections of data concisely.
A
lambdafunction is a small, anonymous, one-line function that can take any number of arguments but can only have a single expression.
users = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
# Sorting a list of dictionaries by a specific key
sorted_users = sorted(users, key=lambda user: user["age"])
# user["age"] - It tells Python: "Look inside each dictionary, grab the value associated with the 'age' key, and use that number to determine the sort order.
The
map()function applies a specific function to every item in an iterable (like a list) and returns an iterator.
# Good use case: You already have a function (e.g., str.upper)
words = ['apple', 'banana', 'cherry']
uppercase_words = list(map(str.upper, words))
# Avoid this: map + lambda
numbers = [1, 2, 3]
squared = list(map(lambda x: x**2, numbers))
# Do this instead (List Comprehension):
numbers = [1, 2, 3]
squared = [x**2 for x in numbers]
The filter() function extracts items from an iterable based on a condition (a function that returns True or False).
# Good use case: Removing None or empty values from a dataset
data = ['hello', '', 'world', None, 'python']
valid_data = list(filter(None, data)) # Removes Falsy values like '' and None
# Avoid this: filter + lambda
# evens = list(filter(lambda x: x % 2 == 0, numbers))
# Do this instead (List Comprehension):
evens = [x for x in numbers if x % 2 == 0]
Q23. What is LEGB scope?
LEGB represents the exact order of priority Python uses to search for variables. When you reference a variable name inside your code, Python scans through four distinct lookup scopes one by one.
If Python finds the variable, it stops looking and uses it. If it scans all four scopes without finding the variable, it throws a
NameError.The Four Scopes of LEGB: Local -> Enclosing -> Global -> Built-in.
Local Scope: Variables defined inside the current function. These are created when the function starts and disappear as soon as the function returns.
def my_func():
x = "Local Variable" # Local to my_func
print(x)
Enclosing Scope: Variables inside the outer function of a nested hierarchy. The inner function can look up into the outer function's scope, but not vice-versa.
def outer_func():
x = "Enclosing Variable"
def inner_func():
print(x) # Python finds 'x' in the Enclosing scope
inner_func()
Global Scope: Variables defined at the absolute top level of your script or module, outside of any functions. These are accessible anywhere inside that specific module.
x = "Global Variable" # Top level
def my_func():
print(x) # Python skips L, finds 'x' in G
Built-in Scope: The innermost core of Python itself. This scope contains pre-loaded keywords, functions, and exception types (like
print(),len(),range(), orValueError). It is always available without imports.Modifying Scopes:
globalvsnonlocal
By default, you can read outer variables, but trying to assign a new value to them just creates a brand new local variable instead. To force Python to modify variables in higher scopes, you use specific keywords:global x: Tells a local scope to directly modify a variable living at the top level (Global).
Q24. What is functools.lru_cache?
functools.lru_cacheis a built-in Python decorator that brings memoization to your functions.It speeds up your code by caching (storing) the results of expensive function calls, so that when the function is called again with the exact same arguments, Python skips the calculations entirely and immediately pulls the result from memory.
The LRU stands for Least Recently Used. This means the cache has a size limit, and once it fills up, Python automatically discards the oldest, least-used entries to keep your memory clean.
from functools import lru_cache
import time
# 1. Apply the cache wrapper (limiting size to the 128 most recent calls)
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# 2. Test performance
start = time.perf_counter()
result = fibonacci(35)
end = time.perf_counter()
print(f"Result: {result}")
print(f"Execution Time: {end - start:.6f} seconds")
The Performance Difference:
• Without @lru_cache: Calculating fibonacci(35) takes around 2 to 5 seconds and millions of function calls.
• With @lru_cache: It takes around 0.00005 seconds and exactly 36 function calls.Q25. Pass by value or reference?
Python is neither pass-by-value nor pass-by-reference.
It uses a mechanism called pass-by-assignment (also widely referred to as pass-by-object-reference or call-by-sharing).
In Python, every single value is an object, and every variable name is just a label (reference) pointing to that object in memory. When you pass an argument to a function, Python copies the label, not the object itself.
Passing Immutable Objects (Acts like Pass-by-Value): Objects like integers, floats, strings, and tuples cannot be changed after they are created. If you try to modify an immutable object inside a function, Python creates a completely new object somewhere else in memory and moves the local variable name to point to it. The original variable outside the function remains untouched.
def modify_number(x):
print(f"Inside (before): x points to ID {id(x)}")
x = 100 # Creates a NEW integer object; x now points to 100
print(f"Inside (after): x points to NEW ID {id(x)}")
num = 10
print(f"Outside (before): num points to ID {id(num)}")
modify_number(num)
print(f"Outside (after): num is still {num}")
• The value of num remains 10.
• Reassigning x = 100 inside the function simply breaks the link between x and 10, attaching x to 100 instead.If you pass a MUTABLE object (Lists, Dictionaries, Sets): It behaves like pass-by-reference. Since the object can be modified in-place, using methods that alter the object (like
.append()or.update()) will directly change the original object living outside the function.
def append_item(my_list):
my_list.append(4) # Modifies the original object in place
numbers = [1, 2, 3]
append_item(numbers)
print(f"Outside: numbers is now {numbers}")
• numbers becomes [1, 2, 3, 4].
• Because both the outside variable numbers and the inside parameter my_list point to the exact same block of memory, changing the contents inside reflects everywhere.
OOP and dataclasses
Q26. What are the four pillars of OOP in Python terms?
Encapsulation, Inheritance, Polymorphism and Abstraction.
These principles form the baseline for writing clean, reusable and maintainable code.
Encapsulation (Data Protection)
Encapsulation is the practice of keeping data (attributes) and methods (functions) into a single unit (a class) while restricting direct access to some components. This prevents accidental data modification.
In Python, access restrictions are established by using underscores:
_variable: Protected (a warning to other developers not to access it outside the class).__variable: Private (triggers "name mangling" so it cannot be accessed directly from outside)
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # Private attribute
# Getter method to safely access private data
def get_balance(self):
return self.__balance
# Setter method to safely modify private data
def deposit(self, amount):
if amount > 0:
self.__balance += amount
else:
print("Invalid deposit amount!")
account = BankAccount("Alice", 1000)
account.deposit(500)
print(account.get_balance()) # Output: 1500
# print(account.__balance) # Throws AttributeErrorInheritance (Code Reusability):
Inheritance allows a child class to derive attributes and methods from a parent class. This prevents code duplication and keeps code DRY (Don't Repeat Yourself).
You can use the
super()function to call methods from the parent class inside the child class.
class Animal: # Parent Class
def eat(self):
print("This animal is eating.")
class Dog(Animal): # Child Class inheriting from Animal
def bark(self):
print("Woof woof!")
my_dog = Dog()
my_dog.eat() # Output: This animal is eating. (Inherited method)
my_dog.bark() # Output: Woof woof! (Child method)Polymorphism (Flexibility):
Polymorphism literally means "many forms". It allows different classes to have methods with the same name but different implementations.
This allows you to write flexible functions that can handle multiple types of objects identically.
class Cat:
def speak(self):
return "Meow"
class Dog:
def speak(self):
return "Woof"
# A function that accepts any object with a 'speak' method
def make_animal_sound(animal_object):
print(animal_object.speak())
cat = Cat()
dog = Dog()
make_animal_sound(cat) # Output: Meow
make_animal_sound(dog) # Output: WoofAbstraction (Hiding Complexity):
Abstraction hides internal complexities and exposes only essential features to the user. For example, when you turn on a car, you only push a button; you don't need to know how the engine works internally.
Python achieves abstraction using the built-in
abc(Abstract Base Classes) module and the@abstractmethoddecorator.An abstract class cannot be instantiated directly; it acts as a blueprint that forces child classes to implement specific methods.
from abc import ABC, abstractmethod
class Vehicle(ABC): # Abstract Base Class
@abstractmethod
def start_engine(self):
pass
class Car(Vehicle):
def start_engine(self):
print("Car engine started. Vroom!")
# my_vehicle = Vehicle() # Throws TypeError (Cannot instantiate abstract class)
my_car = Car()
my_car.start_engine() # Output: Car engine started. Vroom!Q27. What do __init__, __repr__, __str__ and __len__ do?
__init__ initializes the object; __repr__ is the unambiguous developer string; __str__ is the readable string; __len__ powers len(obj). These are dunder (magic) methods.
class Doc:
def __init__(self, text):
self.text = text
def __repr__(self):
return f"Doc({self.text[:20]!r})"
def __len__(self):
return len(self.text)Q28. @classmethod vs @staticmethod vs instance method?
Instance methods get self; class methods get cls and are often alternative constructors; static methods get neither and are just namespaced functions.
class Config:
def __init__(self, model): self.model = model
@classmethod
def from_env(cls):
import os
return cls(os.getenv("MODEL", "default"))
@staticmethod
def is_valid(name): return bool(name)Q29. What is @property?
Lets you expose a method as an attribute, adding validation or computed values without changing the calling code.
class Temp:
def __init__(self, t): self._t = t
@property
def t(self): return self._t
@t.setter
def t(self, v):
if not 0 <= v <= 2:
raise ValueError("temperature must be 0-2")
self._t = vQ30. Inheritance and super()? What is MRO?
super() calls the parent's method. Method Resolution Order (MRO) is the order Python searches classes in multiple inheritance (C3 linearization); view it with Cls.__mro__.
class Base:
def run(self): return "base"
class Child(Base):
def run(self): return "child+" + super().run()Q31. How do you define an abstract class?
Use abc.ABC and @abstractmethod; subclasses must implement the methods or cannot be instantiated.
from abc import ABC, abstractmethod
class LLMClient(ABC):
@abstractmethod
def generate(self, prompt: str) -> str: ...
class OpenAIClient(LLMClient):
def generate(self, prompt): return "..."Q32. What is a dataclass and when to use it?
A decorator that auto-generates __init__, __repr__ and __eq__ for classes that mainly hold data. Use field(default_factory=...) for mutable defaults.
from dataclasses import dataclass, field
@dataclass
class Chunk:
text: str
source: str
metadata: dict = field(default_factory=dict)Q33. Dataclass vs Pydantic model?
Dataclasses give no runtime validation or type coercion. Pydantic validates and parses input (essential for API payloads and LLM structured output), at some speed cost. Use dataclasses for internal plain data, Pydantic at boundaries.
Errors, files, JSON, environment
Q34. How does exception handling work? What do else and finally do?
try runs the code; except handles specific errors; else runs if no error occurred; finally always runs (cleanup). Catch specific exceptions, never a bare except:.
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
print(f"bad JSON: {e}")
else:
print("parsed")
finally:
print("done")Q35. How do you create a custom exception?
class RateLimitError(Exception):
def __init__(self, retry_after: float):
super().__init__(f"retry after {retry_after}s")
self.retry_after = retry_afterQ36. What is a context manager? Write one.
An object that sets up and tears down a resource around a with block. Implement __enter__/__exit__, or use contextlib.contextmanager.
from contextlib import contextmanager
import time
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield
finally:
print(f"{label}: {time.perf_counter() - start:.2f}s")
with timer("embedding"):
...Q37. How do you read and write files safely?
Use with so files always close, specify encoding, and use pathlib.
from pathlib import Path
p = Path("data/notes.txt")
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text("hello", encoding="utf-8")
print(p.read_text(encoding="utf-8"))
with open("big.txt", encoding="utf-8") as f:
for line in f: # streams line by line
process(line)Q38. How do you work with JSON and JSONL?
import json
obj = json.loads('{"a": 1}') # str -> dict
text = json.dumps(obj, indent=2) # dict -> str
with open("data.jsonl", encoding="utf-8") as f:
rows = [json.loads(line) for line in f]Q39. How do you manage API keys?
Never hardcode or commit them. Store in a .env file (git-ignored) or the platform's secret store, and load with python-dotenv.
import os
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.environ["OPENAI_API_KEY"] # raises KeyError if missingQ40. What is a virtual environment and how do you use it?
An isolated Python environment with its own packages, so projects do not conflict.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
pip freeze > requirements.txtQ41. How do you parse command-line arguments?
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True)
parser.add_argument("--top-k", type=int, default=5)
args = parser.parse_args()5. Async and concurrency
Q42. Threading vs multiprocessing vs asyncio: when to use which?
Tool | Best for | Why |
|---|---|---|
| many I/O-bound calls (LLM APIs, HTTP, DB) | single thread, cheap tasks, explicit |
| I/O-bound code using blocking libraries | threads release the GIL while waiting on I/O |
| CPU-bound work (parsing, embeddings on CPU) | separate processes bypass the GIL |
Q43. What do async and await do?
async def defines a coroutine. await pauses it until the awaited operation finishes, letting the event loop run other tasks meanwhile. Nothing runs concurrently until you schedule coroutines as tasks or with gather.
import asyncio
async def fetch(i):
await asyncio.sleep(1) # simulates an API call
return i
async def main():
print(await fetch(1))
asyncio.run(main())Q44. How do you run many LLM calls concurrently?
Use asyncio.gather. Ten 1-second calls finish in about 1 second instead of 10.
async def main():
results = await asyncio.gather(*(fetch(i) for i in range(10)))
print(results)Q45. How do you limit concurrency to respect rate limits?
Use an asyncio.Semaphore.
sem = asyncio.Semaphore(5) # max 5 in flight
async def limited(i):
async with sem:
return await fetch(i)
results = await asyncio.gather(*(limited(i) for i in range(100)))Q46. How do you handle errors and timeouts in async code?
gather(..., return_exceptions=True) returns exceptions as results instead of failing the whole batch; asyncio.wait_for enforces a timeout.
results = await asyncio.gather(*tasks, return_exceptions=True)
ok = [r for r in results if not isinstance(r, Exception)]
try:
await asyncio.wait_for(fetch(1), timeout=5)
except asyncio.TimeoutError:
print("timed out")Q47. What is a common async mistake?
Calling blocking code (time.sleep, requests.get, a sync SDK call) inside async def. It blocks the whole event loop. Use await asyncio.sleep, httpx.AsyncClient, the async SDK client, or await asyncio.to_thread(blocking_fn).
Q48. How do you stream an async generator (LLM tokens)?
async def token_stream():
for tok in ["Hel", "lo", "!"]:
await asyncio.sleep(0.1)
yield tok
async def main():
async for tok in token_stream():
print(tok, end="", flush=True)Q49. How do you run CPU-bound work in parallel?
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as ex:
results = list(ex.map(heavy_fn, items))6. Typing, Pydantic, FastAPI
Q50. Why use type hints if Python ignores them at runtime?
They document intent, enable editor autocomplete, and let tools like mypy or pyright catch bugs before running. Pydantic and FastAPI also read them at runtime.
from typing import Optional
def top_k(scores: list[float], k: int = 3) -> list[int]: ...
def find(doc_id: str) -> Optional[dict]: ... # or dict | NoneQ51. What is Pydantic and why does it matter for AI apps?
A library that validates and parses data against a typed schema, raising clear errors when it does not match. It is the standard way to enforce structured LLM output and API request bodies.
from pydantic import BaseModel, Field
class Answer(BaseModel):
answer: str
confidence: float = Field(ge=0, le=1)
sources: list[str] = []
a = Answer(answer="Paris", confidence="0.9") # "0.9" coerced to 0.9Q52. How do you validate LLM JSON output with Pydantic?
from pydantic import ValidationError
raw = '{"answer": "Paris", "confidence": 1.7}'
try:
ans = Answer.model_validate_json(raw)
except ValidationError as e:
print(e) # confidence must be <= 1 -> retry the LLM call with the errorQ53. How do you write a custom validator?
from pydantic import BaseModel, field_validator
class Query(BaseModel):
text: str
@field_validator("text")
@classmethod
def not_empty(cls, v):
if not v.strip():
raise ValueError("empty query")
return v.strip()Q54. How do you serve a RAG pipeline with FastAPI?
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class AskRequest(BaseModel):
question: str
top_k: int = 5
class AskResponse(BaseModel):
answer: str
sources: list[str]
@app.post("/ask", response_model=AskResponse)
async def ask(req: AskRequest):
try:
answer, sources = await rag_answer(req.question, req.top_k)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return AskResponse(answer=answer, sources=sources)
# run: uvicorn main:app --reloadQ55. What is dependency injection in FastAPI?
Depends() lets you declare shared resources (DB session, vector store, API client) that FastAPI creates per request or reuses, which also makes testing easier by overriding them.
from fastapi import Depends
def get_store():
return vector_store
@app.get("/search")
async def search(q: str, store=Depends(get_store)):
return store.query(q)Q56. How do you stream a response in FastAPI?
from fastapi.responses import StreamingResponse
@app.get("/stream")
async def stream(q: str):
return StreamingResponse(token_stream(), media_type="text/event-stream")Q57. def vs async def endpoints in FastAPI?
Use async def when you await async I/O. Plain def endpoints run in a threadpool, which is safe for blocking libraries. Never put blocking calls inside async def.
7. NumPy and Pandas
Q58. Why is NumPy faster than Python lists?
NumPy arrays store same-typed values in contiguous memory and run operations in compiled C code (vectorization), avoiding Python-level loops.
import numpy as np
x = np.arange(1_000_000)
y = x * 2 + 1 # vectorized, no loopQ59. What is broadcasting?
NumPy's rule for combining arrays of different shapes by stretching size-1 dimensions, so no manual copying is needed.
a = np.ones((3, 4))
b = np.array([1, 2, 3, 4]) # shape (4,)
(a + b).shape # (3, 4)Q60. Compute cosine similarity between two embeddings.
import numpy as np
def cosine(a, b):
a, b = np.asarray(a), np.asarray(b)
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))Q61. Find top-k most similar documents for a query (vectorized).
def top_k(query, matrix, k=3):
# matrix: (n_docs, dim), query: (dim,)
sims = (matrix @ query) / (np.linalg.norm(matrix, axis=1) * np.linalg.norm(query))
idx = np.argpartition(-sims, k)[:k] # O(n) selection
return idx[np.argsort(-sims[idx])] # sort only the k winnersQ62. What is the difference between a view and a copy in NumPy?
Basic slicing returns a view (shares memory, edits affect the original); fancy or boolean indexing returns a copy. Use .copy() when you need independence.
Q63. How do you handle missing data in Pandas?
import pandas as pd
df.isna().sum() # count missing per column
df = df.dropna(subset=["text"]) # drop rows missing text
df["score"] = df["score"].fillna(df["score"].median())Q64. groupby, merge, and apply: show each.
agg = df.groupby("model")["score"].agg(["mean", "std", "count"])
merged = results.merge(meta, on="doc_id", how="left")
df["n_words"] = df["text"].apply(lambda t: len(t.split())) # prefer df["text"].str.split().str.len()Q65. loc vs iloc?
loc selects by label (and boolean masks); iloc selects by integer position.
df.loc[df["score"] > 0.8, ["question", "score"]]
df.iloc[:5, :2]Q66. How do you read large CSV files efficiently?
for chunk in pd.read_csv("big.csv", chunksize=100_000, usecols=["id", "text"]):
process(chunk)Also set dtype to smaller types and use Parquet instead of CSV when possible.
8. LLMs, RAG and agents in Python
Q67. How do you call an LLM API from Python?
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY from the environment
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Answer only from the context."},
{"role": "user", "content": "What is RAG?"},
],
temperature=0.2,
)
print(resp.choices[0].message.content)The pattern is the same for Anthropic and Gemini SDKs: a client, a model name, a list of messages, and sampling parameters.
Q68. How do you stream tokens?
stream = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Explain embeddings."}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)Q69. What are embeddings and how do you generate them?
Fixed-length vectors that place semantically similar text close together. Batch your inputs to reduce latency and cost.
res = client.embeddings.create(model="text-embedding-3-small", input=["cat", "kitten"])
vectors = [d.embedding for d in res.data]Q70. Explain a RAG pipeline end to end.
Ingest: load documents, split into chunks, embed, store in a vector DB. Query: embed the question, retrieve top-k chunks, build a prompt with that context, call the LLM, return the answer with sources. Evaluate retrieval and generation separately.
def answer(question: str, k: int = 4) -> str:
q_vec = embed([question])[0]
chunks = store.search(q_vec, k=k)
context = "\n\n".join(c.text for c in chunks)
prompt = f"Context:\n{context}\n\nQuestion: {question}\nAnswer using only the context."
return llm(prompt)Q71. How do you chunk text? What trade-offs matter?
Small chunks improve retrieval precision but lose context; large chunks keep context but dilute relevance and cost more tokens. Overlap avoids cutting an idea in half. Prefer splitting on structure (paragraphs, headings) over fixed characters.
def chunk(text: str, size: int = 800, overlap: int = 100) -> list[str]:
step = size - overlap
return [text[i:i + size] for i in range(0, max(len(text) - overlap, 1), step)]Q72. How does a vector database search work? Show a minimal FAISS example.
It finds nearest neighbors of the query vector by similarity (cosine, dot product, L2). Large stores use approximate indexes (HNSW, IVF) to trade a little recall for big speed gains.
import faiss, numpy as np
vecs = np.random.rand(1000, 384).astype("float32")
faiss.normalize_L2(vecs) # cosine via inner product
index = faiss.IndexFlatIP(384)
index.add(vecs)
q = vecs[:1].copy()
scores, ids = index.search(q, 5)Q73. How do you get reliable structured output from an LLM?
Prefer the provider's structured output or JSON-schema mode, validate with Pydantic, and retry with the validation error fed back to the model if parsing fails.
def get_structured(prompt: str, retries: int = 2) -> Answer:
for _ in range(retries + 1):
raw = llm(prompt + "\nReturn JSON only.")
try:
return Answer.model_validate_json(raw)
except ValidationError as e:
prompt += f"\nPrevious output was invalid: {e}. Fix it."
raise RuntimeError("no valid output")Q74. How does tool calling (function calling) work?
You describe functions with a JSON schema. The model returns a request to call one with arguments; your code runs it and sends the result back; the model then writes the final answer. An agent is this loop repeated until the model stops asking for tools.
import json
TOOLS = {"get_weather": lambda city: f"28C in {city}"}
def agent_loop(messages, max_steps=5):
for _ in range(max_steps): # always cap the loop
msg = call_llm_with_tools(messages)
if not msg.tool_calls:
return msg.content
messages.append(msg)
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
result = TOOLS[call.function.name](**args)
messages.append({"role": "tool", "tool_call_id": call.id, "content": str(result)})
return "stopped: max steps reached"Q75. How do you count tokens and control cost?
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
n_tokens = len(enc.encode("some prompt text"))Cap max_tokens, trim or summarize history, cache repeated calls, batch embeddings, and use a smaller model for easy steps.
Q76. How do you evaluate a RAG system?
Evaluate retrieval and generation separately. Retrieval: recall@k, precision@k, MRR. Generation: faithfulness (is the answer supported by the context), answer relevance, and correctness against a labeled set. LLM-as-judge works at scale but must be calibrated against human labels.
def recall_at_k(retrieved: list[str], relevant: set[str], k: int) -> float:
return len(set(retrieved[:k]) & relevant) / len(relevant)
def mrr(retrieved: list[str], relevant: set[str]) -> float:
for rank, doc in enumerate(retrieved, start=1):
if doc in relevant:
return 1 / rank
return 0.0Q77. How do you reduce hallucinations in a RAG app?
Better retrieval (hybrid search, reranking), an instruction to answer only from context and say "I don't know", low temperature, citations of source chunks, and a faithfulness check. Add guardrails against prompt injection in retrieved text.
Q78. What is hybrid search and reranking?
Hybrid search combines keyword (BM25) and vector search, merging scores (for example reciprocal rank fusion). A reranker (cross-encoder) rescoring the top 20-50 results improves precision at extra latency.
def rrf(rank_lists: list[list[str]], k: int = 60) -> list[str]:
scores: dict[str, float] = {}
for ranks in rank_lists:
for pos, doc in enumerate(ranks, start=1):
scores[doc] = scores.get(doc, 0) + 1 / (k + pos)
return sorted(scores, key=scores.get, reverse=True)9. Testing, logging, retries, project structure
Q79. How do you write tests with pytest?
Name files test_*.py, write plain asserts, use fixtures for setup and parametrize for many cases.
import pytest
from mylib import chunk
@pytest.fixture
def text():
return "a" * 2000
@pytest.mark.parametrize("size,expected", [(1000, 3), (500, 5)])
def test_chunk_count(text, size, expected):
assert len(chunk(text, size=size, overlap=0)) >= expected - 1
def test_empty():
with pytest.raises(ValueError):
chunk(None)Q80. How do you test code that calls an LLM?
Never hit the real API in unit tests: mock the client so tests are fast, free, and deterministic. Keep a small separate set of evaluation runs for real model behavior.
from unittest.mock import patch
@patch("app.rag.llm", return_value="Paris")
def test_answer(mock_llm):
assert answer("Capital of France?") == "Paris"
mock_llm.assert_called_once()Q81. Why use logging instead of print?
Levels (DEBUG to ERROR), timestamps, handlers (file, console), and per-module control. Log latency, token counts, and request IDs in LLM apps.
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
log = logging.getLogger(__name__)
log.info("retrieved %d chunks in %.2fs", len(chunks), elapsed)
log.exception("llm call failed") # includes tracebackQ82. Implement retries with exponential backoff and jitter.
Used for rate limits (429) and transient 5xx errors. Do not retry on invalid-request errors.
import random, time
from functools import wraps
def backoff(max_tries=5, base=1.0, retry_on=(Exception,)):
def deco(fn):
@wraps(fn)
def wrapper(*a, **kw):
for attempt in range(max_tries):
try:
return fn(*a, **kw)
except retry_on:
if attempt == max_tries - 1:
raise
time.sleep(base * 2 ** attempt + random.random())
return wrapper
return decoQ83. How do you cache LLM or embedding results?
Hash the input (plus model and params) and store the result in memory, disk, or Redis. Embeddings of unchanged text should never be recomputed.
import hashlib, json
from pathlib import Path
CACHE = Path(".cache"); CACHE.mkdir(exist_ok=True)
def cached(model: str, text: str, fn):
key = hashlib.sha256(f"{model}:{text}".encode()).hexdigest()
f = CACHE / f"{key}.json"
if f.exists():
return json.loads(f.read_text())
out = fn(text)
f.write_text(json.dumps(out))
return outQ84. What does a clean AI project structure look like?
project/
src/app/
api.py # FastAPI routes
rag/ # ingest.py, retrieve.py, generate.py
schemas.py # Pydantic models
config.py # settings from env
tests/
evals/ # eval datasets + scripts
.env.example
pyproject.toml / requirements.txt
README.mdKeep config out of code, separate I/O from logic so it is testable, and pin dependency versions.
10. Coding round: 10 common problems
State the approach and complexity out loud before coding.
Q85. Two Sum: return indices of two numbers adding to a target.
One pass with a hash map. Time O(n), space O(n).
def two_sum(nums, target):
seen = {}
for i, n in enumerate(nums):
if target - n in seen:
return [seen[target - n], i]
seen[n] = iQ86. Are two strings anagrams?
Time O(n).
from collections import Counter
def is_anagram(a, b):
return Counter(a) == Counter(b)Q87. Group anagrams together.
Use the sorted word as the key. Time O(n k log k).
from collections import defaultdict
def group_anagrams(words):
groups = defaultdict(list)
for w in words:
groups["".join(sorted(w))].append(w)
return list(groups.values())Q88. Valid parentheses.
Use a stack. Time O(n).
def valid(s):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for c in s:
if c in pairs:
if not stack or stack.pop() != pairs[c]:
return False
else:
stack.append(c)
return not stackQ89. First non-repeating character.
from collections import Counter
def first_unique(s):
counts = Counter(s)
for i, c in enumerate(s):
if counts[c] == 1:
return i
return -1Q90. Longest substring without repeating characters.
Sliding window with last-seen index. Time O(n).
def longest_unique(s):
last, start, best = {}, 0, 0
for i, c in enumerate(s):
if c in last and last[c] >= start:
start = last[c] + 1
last[c] = i
best = max(best, i - start + 1)
return bestQ91. Merge overlapping intervals.
Sort by start, then extend or append. Time O(n log n).
def merge(intervals):
out = []
for s, e in sorted(intervals):
if out and s <= out[-1][1]:
out[-1][1] = max(out[-1][1], e)
else:
out.append([s, e])
return outQ92. Implement an LRU cache.
OrderedDict keeps order; get and put are O(1). Very common in AI-engineer rounds (caching LLM responses).
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.cap = capacity
self.data = OrderedDict()
def get(self, key):
if key not in self.data:
return -1
self.data.move_to_end(key)
return self.data[key]
def put(self, key, value):
self.data[key] = value
self.data.move_to_end(key)
if len(self.data) > self.cap:
self.data.popitem(last=False) # evict least recently usedQ93. Flatten an arbitrarily nested list.
def flatten(items):
for x in items:
if isinstance(x, list):
yield from flatten(x)
else:
yield x
list(flatten([1, [2, [3, 4]], 5])) # [1, 2, 3, 4, 5]Q94. Top-k most frequent words.
most_common uses a heap internally, roughly O(n log k).
from collections import Counter
import re
def top_words(text, k=3):
words = re.findall(r"[a-z']+", text.lower())
return Counter(words).most_common(k)Q95. Rapid-fire theory questions to prepare
listvsarraymodule vs NumPy array?listholds any objects;array.arrayholds one primitive type compactly; NumPy adds vectorized math and n-dimensional shapes.What is
__slots__? It fixes an object's attributes up front, removing the per-instance__dict__and cutting memory.How does Python manage memory? Reference counting frees objects at zero references; a cyclic garbage collector handles reference cycles.
What is monkey patching? Replacing attributes or functions at runtime. Acceptable mainly in tests (
unittest.mock.patch), risky elsewhere.dequevslistfor queues?collections.dequepops from both ends in O(1);list.pop(0)is O(n).yield fromandsend()?yield fromdelegates to a sub-generator;send()pushes a value into a paused generator (the basis of old-style coroutines).What do
__call__,__getitem__,__iter__,__enter__do? They make objects callable like functions, indexable (obj[i]), iterable, and usable inwithblocks.Why is
x in listslower thanx in set? A list scans every element, O(n); a set hashes the value and jumps to it, O(1) on average.What is a race condition? Two threads read and modify shared state in an unpredictable order. Use a
threading.Lock(or avoid shared mutable state) to prevent it.