Which built-in Python data types are sequences and what characterizes a sequence?
Built-in sequence types are:
- str
- list
- tuple
- range
A sequence is an ordered collection that:
- Preserves element order
- Supports indexing (s[0])
- Supports slicing (s[1:4])
- Can be iterated over
What is the difference between a stack and a queue, and when should each be used?
Stack → LIFO (Last In, First Out)
- push, pop
- Used for recursion, undo operations
Queue → FIFO (First In, First Out)
- enqueue, dequeue
- Used for scheduling, task processing, BFS
What is a deque in Python?
A deque (double-ended queue) from collections allows:
- Fast append() and pop()
- Fast appendleft() and popleft()
Efficient for insertions/removals at both ends.
Which operations are less efficient in a deque compared to a list?
Random access (d[i]) is slower in a deque.
Lists allow O(1) indexing due to contiguous memory.
Deques are optimized for fast end operations, not middle access.
What is the difference between an iterable and an iterator?
Iterable:
- An object you can loop over
- Implements __iter__()
Iterator:
- Created using iter()
- Produces items with next()
- Keeps state
- Raises StopIteration when finished
What can be used as a context manager and why?
Any object implementing:
- __enter__()
- __exit__()
Used with "with" to:
- Automatically manage resources
- Ensure cleanup (files, DB connections)
What are magic methods and when are they used?
Magic methods start and end with __ (e.g., __init__, __str__).
They define behavior for:
- Object creation
- Arithmetic
- Comparison
- Printing
They enable operator overloading.
How can you mark a function as private in Python?
You cannot make it truly private.
Convention:
- _function() → protected
- __function() → name mangling (class only)
Privacy is by convention, not enforced.
What does the zip() function do?
zip() combines multiple iterables element-wise.
Returns an iterator of tuples.
Example:
zip([1,2], ['a','b']) → (1,'a'), (2,'b')
What does functools.wraps do?
Used in decorators.
It preserves:
- Original function name
- Docstring
- Metadata
Without it, the wrapper replaces function metadata.
What are generator expressions?
A compact way to create generators:
(x*x for x in range(5))
They:
- Produce values lazily
- Use less memory
- Return generator objects
What is a closure in Python?
A closure is a function that:
- Remembers variables from its enclosing scope
- Even after the outer function has finished
Used for function factories and data encapsulation.
What is a dataclass and its advantages?
A @dataclass automatically generates:
- __init__
- __repr__
- __eq__
Advantages:
- Less boilerplate
- Cleaner code
- Designed for storing data
What does it mean that Python is strongly typed and dynamically typed?
Strongly typed:
- No implicit type coercion (e.g., "3" + 3 fails)
Dynamically typed:
- Type is determined at runtime
- No need to declare variable types
What is Python’s naming convention (PEP 8)?
- variables/functions: lowercase_with_underscores
- Classes: CamelCase
- Constants: UPPERCASE
Name three built-in data types in Python.
Examples:
- int
- float
(others: list, dict, set, tuple, bool)
What is a module in Python?
A module is a .py file containing:
- Functions
- Classes
- Variables
Used to organize and reuse code.
Where does Python look for modules?
Python searches:
- Current directory
- PYTHONPATH
- Standard library directories
- Installed packages (site-packages)
Stored in sys.path.
What does the finally clause do in try/except?
The finally block:
- Always executes
- Runs whether an exception occurs or not
Used for cleanup operations.
Can default parameters appear before non-default ones?
No.
Default parameters must come after required parameters.
Correct:
def f(a, b=3)
What is the difference between positional and keyword arguments?
Positional:
- Order matters
Keyword:
- Passed using parameter names
- Order does not matter
How do you accept an arbitrary number of arguments?
*args → variable positional arguments (tuple)
**kwargs → variable keyword arguments (dict)
How can you unpack collections into function arguments?
Use:
* for sequences
** for dictionaries
func(*my_list)
func(**my_dict)
How do you sort a list by the last letter of each string?
Use sorted() with key:
sorted(strings, key=lambda s: s[-1])
What is the difference between append() and extend()?
append(x):
- Adds one element
- List grows by one
extend(iterable):
- Adds multiple elements
- Merges another iterable
What does list slicing [x:y:z] mean?
x → start index
y → stop index (exclusive)
z → step size
[1:10:2] → every second element
What does my_list[1:] return for a list of 20 elements?
It returns:
- All elements starting from index 1
- Excludes the first element
- Total length = 19
What are namedtuple, Counter, and OrderedDict?
namedtuple:
- Tuple with named fields
Counter:
- Counts element frequency
OrderedDict:
- Dictionary preserving insertion order (older Python versions)
How do you specify a variable type?
Using type hints:
x: int = 3
Type hints improve readability and static analysis.
How do you specify a function return type?
Using ->
def func() -> int:
return 3
What are decorators?
Decorators:
- Wrap functions
- Add functionality
- Use @syntax
They modify behavior without changing original code.
Compare list, tuple, set, and dict (2 characteristics each).
list:
- Ordered
- Mutable
tuple:
- Immutable
set:
- Unordered
- Unique elements
dict:
- Key-value pairs
Name two ways to format strings in Python.
1. f-strings:
f"Hello {name}"
2. str.format():
"Hello {}".format(name)
What do the positions in string[x:y:z] stand for?
x -> start position (inclusive)
y -> end position (exclusive)
z -> step size
Name two different ways to format strings in Python.
f-strings (f"text {variable}")
.format()
method ("text {}".format(variable))
Write an f-string that prints "Alice is 25 years old" using variables name = "Alice" and age = 25.
print(f"{name} is {age} years old.")
Write code using .format() to print "Hello, Bob!" using the variable name = "Bob".
print("Hello, {}!".format(name))
What do %s, %d, and %f mean in % formatting?
%s = string
%d = integer (decimal)
%f = float
Example: "Name: %s, Age: %d" % (name, age)
How do you format a float to 2 decimal places using f-strings?
price = 19.99
print(f"{price:.2f}") # Output: 19.99
What's the difference between append() and extend()?
append(x) — adds ONE element (even if it's a list)
extend(iterable) — adds MULTIPLE elements from an iterable
my_list = [1, 2]
my_list.append([3, 4]) # → [1, 2, [3, 4]] ← nested list!
my_list.extend([3, 4]) # → [1, 2, 3, 4] ← flattened
What's the difference between pop() and remove()?
pop(i) — removes by INDEX (default: last), RETURNS the value
remove(x) — removes by VALUE (first occurrence), returns None
my_list = ['a', 'b', 'c']
my_list.pop(1) # Removes index 1 → returns 'b'
my_list.remove('a') # Removes value 'a' → returns None
Which list methods modify the list in-place (don't return a new list)?
reverse() — reverses in-place
sort() — sorts in-place
append(), extend(), insert(), remove(), pop(), clear()
⚠️ Common mistake: my_list.sort() returns None, NOT the sorted list!
What does "shallow copy" mean for list.copy()?
Copies the list structure, but NOT nested objects (e.g., inner lists)
Changes to nested objects affect BOTH lists
list1 = [[1, 2], [3, 4]]
list2 = list1.copy()
list1[0][0] = 99 # ← Changes list2 too!
# list1 = [[99, 2], [3, 4]]
# list2 = [[99, 2], [3, 4]] ← affected!
What do index() and count() return?
index(x) — index of FIRST occurrence (error if not found)
count(x) — number of occurrences (0 if not found)
my_list = ['a', 'b', 'a', 'c']
my_list.index('a') # → 0 (first occurrence)
my_list.count('a') # → 2 (appears twice)
What does insert(i, x) do? What if index is out of bounds?
Inserts element x BEFORE index i. If i is out of bounds, appends to start/end (no error).
my_list.insert(0, 'z') # → ['z', 'a', 'b', 'c']
my_list.insert(100, 'end') # → [..., 'c', 'end'] ← at end
Write the syntax for list, dict, and set comprehensions.
# List comprehension
[expression for item in iterable if condition]
# Dict comprehension
{key: value for item in iterable if condition}
# Set comprehension
{expression for item in iterable if condition}
Write a list comprehension to get squares of even numbers from [1, 2, 3, 4, 5].
numbers = [1, 2, 3, 4, 5]
result = [x**2 for x in numbers if x % 2 == 0]
# Result: [4, 16]
What's the difference between {x for x in nums}and [x for x in nums]?
{x for x in nums}
[x for x in nums]
{x for x in nums} — set comprehension → removes duplicates, unordered
[x for x in nums] — list comprehension → keeps duplicates, ordered
nums = [1, 2, 2, 3]
list_comp = [x for x in nums] # → [1, 2, 2, 3]
set_comp = {x for x in nums} # → {1, 2, 3}
What's the difference between a module and a package in Python?
Module: A single .py file containing Python code
Package: A directory containing modules +__init__.py file
__init__.py
Module: math_utils.py
math_utils.py
Package:
mypackage/(directory) with__init__.py inside
mypackage/
What is the purpose of __init__.py in a package?
Marks a directory as a package (traditional packages)
Can be empty OR contain initialization code
Namespace packages (Python 3.3+) don't require __init__.py
Note: Without __init__.py, the directory is a namespace package (more advanced)
Briefly describe namedtuple, Counter, OrderedDict, and deque from the collections module.
namedtuple
Counter
OrderedDict
deque
collections
namedtuple— tuple with named fields (access by name:point.x)
point.x
Counter— counts occurrences of elements in an iterable
OrderedDict— dict that preserves insertion order (+ ordering methods)
deque— double-ended queue, fast append/pop from both ends
Easy memory trick:
namedtuple = group variables together
Counter = count collection items
OrderedDict = preserve insertion order
deque = efficient stack/queue
Explain when try, except, else, and finally blocks execute.
try
except
else
finally
try— code that might raise an exception
except— runs if exception occurs in try
else— runs ONLY if NO exception occurred
finally— ALWAYS runs (cleanup code)
Order: try → except (if error) → else (if no error) → finally (always)
Show two ways to catch multiple exception types.
# Method 1: Separate blocks
try:
...
except ValueError:
except TypeError:
# Method 2: Single block with tuple
except (ValueError, TypeError):
How do you manually raise an exception in Python?
Use the raise keyword:
raise
raise ValueError("Custom error message")
# Or re-raise the current exception:
except SomeError:
# do something
raise # re-raises the same exception
Where must default parameters appear in a function definition?
Default parameters MUST come AFTER non-default parameters.
# ✅ Correct
def func(a, b, c=5, d=10):
pass
# ❌ Wrong
def func(a, c=5, b): # SyntaxError!
What are *args and **kwargs? What data types do they create?
*args
**kwargs
*args— collects extra positional arguments into a tuple
**kwargs— collects extra keyword arguments into a dict
def example(*args, **kwargs):
print(type(args)) # → <class 'tuple'>
print(type(args))
# → <class 'tuple'>
print(type(kwargs)) # → <class 'dict'>
print(type(kwargs))
# → <class 'dict'>
example(1, 2, 3, a=4, b=5)
# args = (1, 2, 3)
# kwargs = {'a': 4, 'b': 5}
How do you unpack a list and a dict as function arguments?
*list— unpacks list/tuple as positional arguments
*list
**dict— unpacks dict as keyword arguments
**dict
def func(a, b, c):
return a + b + c
values = [1, 2, 3]
func(*values) # Same as: func(1, 2, 3)
params = {"a": 1, "b": 2, "c": 3}
func(**params) # Same as: func(a=1, b=2, c=3)
When calling a function, what's the rule about positional and keyword arguments?
Positional arguments MUST come BEFORE keyword arguments.
func(1, 2, c=3) # ✅ Correct
func(1, 2, c=3)
func(1, b=2, c=3) # ✅ Correct
func(1, b=2, c=3)
func(a=1, 2, 3) # ❌ WRONG! keyword before positional
func(a=1, 2, 3)
# ❌ WRONG! keyword before positional
What is the syntax of a lambda function? What are its limitations?
lambda parameters: expression
Limitations:
Only one expression (no multiple statements, no loops)
Implicitly returns the expression result
Noreturn keyword needed/allowed
return
lambda x: x ** 2 is the same as def f(x): return x ** 2
lambda x: x ** 2
def f(x): return x ** 2
Sort the list [("Alice", 25), ("Bob", 20)] by age using a lambda.
[("Alice", 25), ("Bob", 20)]
students = [("Alice", 25), ("Bob", 20)]
sorted_students = sorted(students, key=lambda x: x[1])
# Result: [('Bob', 20), ('Alice', 25)]
Pattern:
sorted(iterable, key=lambda x: x[index/attribute])
What's the difference between map() and filter() with lambdas?
map()
filter()
map(func, iterable)— transforms each element, returns all
map(func, iterable)
filter(func, iterable)— keeps only elements where func returnsTrue
filter(func, iterable)
True
nums = [1, 2, 3, 4, 5]
# map: transform all elements
list(map(lambda x: x ** 2, nums))
# → [1, 4, 9, 16, 25] (all 5 transformed)
# filter: keep only some elements
list(filter(lambda x: x % 2 == 0, nums))
# → [2, 4] (only even numbers kept)
What does it mean that functions are "first-class objects" in Python?
Functions can be:
Assigned to variables
Passed as arguments to other functions
Returned from functions
Stored in data structures (lists, dicts)
python
def greet(name):
return f"Hi, {name}"
# Assign to variable
my_func = greet
# Pass as argument
def call_func(func, arg):
return func(arg)
call_func(greet, "Alice") # → "Hi, Alice"
call_func(greet, "Alice")
# → "Hi, Alice"
Write a function apply_twice(func, value) that applies a function to a value twice.
apply_twice(func, value)
def apply_twice(func, value):
return func(func(value))
def square(x):
return x ** 2
apply_twice(square, 2) # → 16
apply_twice(square, 2)
# → 16
# First: square(2) = 4
# Second: square(4) = 16
What's the difference between an iterable and an iterator? What methods must each implement?
Can be looped over (lists, strings, etc.)
Implements__iter__()→ returns an iterator
__iter__()
Can be iterated multiple times
Represents a stream of data
Implements__iter__()(returns self) AND__next__()
__next__()
RaisesStopIterationwhen exhausted
StopIteration
One-time use — exhausted after iteration
Explain what happens behind the scenes when you write for item in my_list.
for item in my_list
Python calls iter(my_list)→ gets an iterator by calling __iter__()
iter(my_list)
In each iteration, Python calls next(iterator)→ gets next value via __next__()
next(iterator)
When __next__()raises StopIteration, the loop terminates
# for item in my_list:
# print(item)
# Equivalent to:
iterator = iter(my_list)
while True:
item = next(iterator)
print(item)
except StopIteration:
break
What are magic/dunder methods in Python? Give 4 examples.
Methods that start and end with double underscores (__). They allow you to customize how Python's built-in operations work with your custom classes.
__
4 Examples:
__str__— called bystr()andprint()
__str__
str()
print()
__init__— constructor, called when creating object
__init__
__eq__— called by==operator
__eq__
==
__len__— called bylen()
__len__
len()
(Other valid answers:__repr__,__add__,__lt__,__iter__,__next__,__call__,__enter__,__exit__)
__repr__
__add__
__lt__
__iter__
__next__
__call__
__enter__
__exit__
Which dunder method is called for each operation?
len(obj) # → obj.__len__()
len(obj)
# → obj.__len__()
str(obj) # → obj.__str__()
str(obj)
# → obj.__str__()
obj1 == obj2 # → obj1.__eq__(obj2)
obj1 == obj2
# → obj1.__eq__(obj2)
obj1 + obj2 # → obj1.__add__(obj2)
obj1 + obj2
# → obj1.__add__(obj2)
obj1 < obj2 # → obj1.__lt__(obj2)
obj1 < obj2
# → obj1.__lt__(obj2)
for x in obj: # → obj.__iter__()
for x in obj:
# → obj.__iter__()
next(iterator) # → iterator.__next__()
# → iterator.__next__()
obj() # → obj.__call__()
obj()
# → obj.__call__()
What's the difference between __str__ and __repr__?
__str__— human-readable, friendly string (for print(),str())
__repr__— unambiguous, developer-focused representation (for repr(), REPL)
repr()
class Point:
def __str__(self):
return f"Point at ({self.x}, {self.y})"
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
p = Point(3, 4)
print(p) # → "Point at (3, 4)" (uses __str__)
repr(p) # → "Point(x=3, y=4)" (uses __repr__)
Given this basic decorator, modify wrapper to accept arbitrary arguments and keyword arguments:
wrapper
def decorator(func):
def wrapper():
print('Execution started')
result = func()
print('Execution completed')
return result
return wrapper
def wrapper(*args, **kwargs): # ← Add *args, **kwargs
result = func(*args, **kwargs) # ← Pass them to func
What is a decorator in Python? How is @decorator syntax used?
@decorator
A decorator is a function that takes a function and returns a modified version of it.
def my_func():
my_func = decorator(my_func)
Pattern: Outer function returns inner wrapper function (closure).
What are the two ways to implement decorators?
Function-based (most common):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
Class-based:
class Decorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
What does functools.wraps do and why is it needed?
functools.wraps
Preserves the original function's metadata (__name__, __doc__, etc.) when decorating.
__name__
__doc__
from functools import wraps
@wraps(func) # ← Preserves func.__name__, func.__doc__
Without it: decorated function has __name__ = "wrapper"instead of the original name.
__name__ = "wrapper"
Describe closures in Python. (bullet points)
A closure is when an inner function has access to variables from its outer function
The inner function "remembers" the outer variables even after the outer function returns
Inner functions can be nested inside outer functions
This is the mechanism behind function-based decorators
def outer(x):
def inner(y):
return x + y # inner accesses x from outer
return inner
add_5 = outer(5)
print(add_5(3)) # → 8 (remembers x=5)
How are closures related to decorators?
Decorators work because of closures:
The decorator'swrapperfunction is an inner function
It retains access tofunc(the decorated function) from the outer scope
func
Whenwrapperis called later, it can still callfunc
def decorator(func): # Outer function
def wrapper(*args, **kwargs): # Inner function
return func(*args, **kwargs) # Uses func (closure!)
What methods must a class implement to be used as a context manager?
__enter__(self)— called when enteringwithblock, returns value forasvariable
__enter__(self)
with
as
__exit__(self, exc_type, exc_val, exc_tb)— called when exiting, handles cleanup
__exit__(self, exc_type, exc_val, exc_tb)
class MyContext:
def __enter__(self):
# Setup code
return self # or any value
def __exit__(self, exc_type, exc_val, exc_tb):
# Cleanup code (always runs!)
return False # propagate exceptions
How do you create a function-based context manager using @contextmanager?
@contextmanager
Use @contextmanager decorator with yield:
yield
from contextlib import contextmanager
def my_context():
# Setup code (before yield)
print("Entering")
yield # or yield value for 'as'
# Code block runs here
finally:
# Cleanup code (after yield)
print("Exiting")
# Usage:
with my_context():
print("Inside")
What's the difference between a list comprehension and a generator expression?
List comprehension ([]) — creates entire list in memory immediately:
[]
[x**2 for x in range(10)] # → [0, 1, 4, 9, ...]
Generator expression (()) — creates values on demand (lazy):
()
(x**2 for x in range(10)) # → generator object
Benefit: Generators are memory-efficient for large datasets.
What does "lazy evaluation" mean in the context of generators?
Values are produced on demand, not all at once.
Generator doesn't compute values until you ask for them (next()or loop)
next()
Memory efficient — doesn't store all values
Can represent infinite sequences
# Only computes values as needed:
gen = (x**2 for x in range(1_000_000))
next(gen) # Only computes first value
How would you re-implement enumerate() and zip() using generators?
enumerate()
zip()
# enumerate
def my_enumerate(iterable, start=0):
index = start
for item in iterable:
yield (index, item)
index += 1
# zip
def my_zip(*iterables):
iterators = [iter(it) for it in iterables]
values = [next(it) for it in iterators]
yield tuple(values)
Show the syntax for type hints on variables and functions.
# Variable type hints
name: str = "Alice"
age: int = 25
# Function parameter and return type hints
def greet(name: str, age: int) -> str:
return f"Hello, {name}! Age: {age}"
# Function with no return value
def print_data(data: list) -> None:
print(data)
Does Python enforce type hints at runtime?
No! Type hints are optional annotations for documentation and static analysis only.
def add(a: int, b: int) -> int:
return a + b
# This runs without error, even with wrong types:
add("hello", "world") # → "helloworld"
To check types: Use a static type checker like mypy, not Python itself.
mypy
What does the @dataclass decorator do? What methods does it auto-generate?
@dataclass
The @dataclassdecorator automatically generates boilerplate methods for classes that mainly store data.
Auto-generated methods:
__init__()— constructor from field annotations
__init__()
__repr__()— string representation
__repr__()
__eq__()— equality comparison
__eq__()
from dataclasses import dataclass
x: int
y: int
# No need to write __init__, __repr__, __eq__ manually!
print(p) # Point(x=3, y=4)
Convert this regular class to a dataclass:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
name: str
age: int
# That's it! __init__, __repr__, __eq__ are auto-generated
Explain why NumPy is faster than Python lists. (Give 3 reasons)
Contiguous memory layout — values stored next to each other → CPU cache friendly
SIMD vectorized operations — CPU can process multiple values in one instruction
Written in C — compiled C code under the hood, not interpreted Python
# Example: NumPy is ~100x faster
import numpy as np
arr = np.arange(1_000_000)
result = arr * 2 # Uses SIMD, contiguous memory, C code
What are the downsides/limitations of NumPy arrays?
Homogeneous types — all elements must be same type (mixing types forces coercion)
Fixed size — adding/removing elements is costly (creates new array)
Integer overflow — fixed-size integers can overflow (unlike Python's arbitrary precision)
# Overflow example:
arr = np.array([127], dtype=np.int8)
arr[0] += 1
print(arr) # → [-128] ← Overflow!
Name the main NumPy data types and how to check an array's type.
Integers:int8,int16,int32,int64(signed),uint8,uint16,uint32,uint64(unsigned)
int8
int16
int32
int64
uint8
uint16
uint32
uint64
Floats:float16,float32,float64
float16
float32
float64
Others:bool,object(avoid — loses performance!)
bool
object
Check type:
arr.dtype # → int64, float64, etc.
arr = np.array([1, 2, 3], dtype=np.int8)
print(arr.dtype) # → int8
What's the difference between np.zeros(), np.ones(), np.full(), and np.eye()?
np.zeros()
np.ones()
np.full()
np.eye()
np.zeros(shape)— array filled with 0
np.zeros(shape)
np.ones(shape)— array filled with 1
np.ones(shape)
np.full(shape, value)— array filled with any value
np.full(shape, value)
np.eye(n)— n×n identity matrix (diagonal 1s, rest 0s)
np.eye(n)
np.zeros(3) # → [0. 0. 0.]
np.ones(3) # → [1. 1. 1.]
np.full(3, 7) # → [7 7 7]
np.eye(3) # → [[1. 0. 0.]
# [0. 1. 0.]
# [0. 0. 1.]]
What are the 4 ways to index NumPy arrays?
Basic indexing:arr[1],arr[1, 2]
arr[1]
arr[1, 2]
Slicing:arr[1:3],arr[:, 1]
arr[1:3]
arr[:, 1]
Boolean indexing:arr[arr > 5](filter by condition)
arr[arr > 5]
Fancy indexing:arr[[0, 2, 4]](select specific indices)
arr[[0, 2, 4]]
What are the two uses of np.where()?
np.where()
1. Find indices where condition is True:
indices = np.where(arr > 10)
arr[indices] # Get those values
2. Conditional replacement (like ternary operator):
# Replace values > 10 with 100, else keep original
result = np.where(arr > 10, 100, arr)
Explain NumPy's broadcasting rules. Are shapes (3, 1) and (1, 4) compatible? What about (3, 2) and (3, 3)?
3 Rules:
If different ndim, prepend 1s to smaller shape
Compatible if: same size OR one is 1 in each dimension
Result shape = max of each dimension
(3, 1) + (1, 4):
Dim 0: 3 vs 1 → compatible ✓
Dim 1: 1 vs 4 → compatible ✓
Result: (3, 4) ✅
(3, 2) + (3, 3):
Dim 0: 3 vs 3 → compatible ✓
Dim 1: 2 vs 3 → incompatible ❌ (neither is 1)
Fails! ❌
What's the difference between a view and a copy? Which operations create each?
View — references same data, changes affect original Copy — independent data, changes don't affect original
Create VIEWS:
Slicing: arr[1:4]
arr[1:4]
Reshape: arr.reshape(2, 3)
arr.reshape(2, 3)
Transpose: arr.T
arr.T
Create COPIES:
.copy():arr.copy()
.copy()
arr.copy()
Fancy indexing: arr[[0, 2, 4]]
Boolean indexing: arr[arr > 5]
arr = np.array([1, 2, 3, 4])
view = arr[1:3]
view[0] = 999
print(arr) # → [1 999 3 4] ← Original changed!
copy = arr.copy()
copy[0] = 111
print(arr) # → [1 999 3 4] ← Original unchanged
What are np.nan, np.inf, and how do you check for them?
np.nan
np.inf
np.nan— Not a Number (missing/undefined values)
np.inf— Positive infinity
np.NINFor-np.inf— Negative infinity
np.NINF
-np.inf
np.pi— π (3.14159...)
np.pi
np.e— Euler's number (2.71828...)
np.e
Checking:
np.isnan(arr) # Check for nan
np.isinf(arr) # Check for infinity
np.isfinite(arr) # Check for finite (not nan, not inf)
⚠️ Important:
np.nan == np.nan # → False! Use np.isnan() instead
What do reshape(-1), expand_dims(), squeeze(), and flatten() do?
reshape(-1)
expand_dims()
squeeze()
flatten()
reshape(-1)— flatten to 1D OR auto-calculate one dimension
arr.reshape(-1) # → 1D
arr.reshape(-1)
# → 1D
arr.reshape(-1, 3) # → auto-calc rows for 3 columns
arr.reshape(-1, 3)
# → auto-calc rows for 3 columns
expand_dims(arr, axis)— add dimension of size 1
expand_dims(arr, axis)
arr.shape: (3,) → expand_dims(arr, 0) → (1, 3)
squeeze()— remove dimensions of size 1
arr.shape: (1, 3, 1) → squeeze() → (3,)
flatten()— convert to 1D (always copy)
arr_2d.flatten() → 1D array (copy, not view)
Explain vstack(), hstack(), and column_stack() with examples.
vstack()
hstack()
column_stack()
vstack()— vertical stack (stack as rows)
np.vstack([[1,2,3], [4,5,6]])
# → [[1 2 3]
# [4 5 6]]
hstack()— horizontal stack (side by side)
np.hstack([[1,2,3], [4,5,6]])
# → [1 2 3 4 5 6]
column_stack()— stack 1D arrays as columns
np.column_stack([[1,2,3], [4,5,6]])
# → [[1 4]
# [2 5]
# [3 6]]
What do .reduce(), .accumulate(), and .outer() do for ufuncs? Is np.vectorize() fast?
.reduce()
.accumulate()
.outer()
np.vectorize()
.reduce()— apply operation across array → single value
np.add.reduce([1,2,3,4]) # → 10 (sum all)
np.add.reduce([1,2,3,4])
# → 10 (sum all)
.accumulate()— cumulative operation → intermediate results
np.add.accumulate([1,2,3,4]) # → [1,3,6,10]
np.add.accumulate([1,2,3,4])
# → [1,3,6,10]
.outer()— apply to all pairs from two arrays
np.multiply.outer([1,2], [10,20])
# → [[10,20], [20,40]]
np.vectorize()— ⚠️ NOT fast! Just a convenience wrapper (essentially a for loop), no performance benefit
Last changed11 hours ago