📋 Basic Slice Assignment: lst[start:stop] = new_values
Replaces a range of elements. The list can grow, shrink, or stay the same size.
# Replace 2 elements with 2 (same size)
lst = [1, 2, 3, 4, 5]
lst[1:3] = ['a', 'b'] # [1, 'a', 'b', 4, 5]
# Replace 3 elements with 1 (shrink)
lst = [1, 2, 3, 4, 5]
lst[1:4] = ['x'] # [1, 'x', 5]
# Replace 2 elements with 4 (grow)
lst = [1, 2, 3, 4, 5]
lst[1:3] = ['a', 'b', 'c', 'd'] # [1, 'a', 'b', 'c', 'd', 4, 5]
Key Point: Basic slice assignment can change list length. The number of new elements doesn't need to match the slice length.
⚡ Step Slice Assignment: lst[start:stop:step] = new_values
Replaces elements at intervals. Size must match exactly!
# Replace every 2nd element (3 positions need 3 values)
lst = [10, 20, 30, 40, 50]
lst[::2] = [1, 2, 3] # [1, 20, 2, 40, 3]
# WRONG: Size mismatch causes ValueError
lst = [1, 2, 3, 4, 5]
lst[::2] = [10, 20] # ValueError! 3 positions, 2 values
Critical Rule: Step slice assignment requires exact size matching. Too few or too many values = ValueError.
🔄 Negative Step Assignment
lst = ['a', 'b', 'c', 'd', 'e']
lst[::-2] = ['x', 'y', 'z'] # Replaces indices 4, 2, 0
# Result: ['z', 'b', 'y', 'd', 'x']
Remember: Negative step goes backwards. Values are assigned in reverse order of indices.
🎯 Empty Slice Insertion
lst = [1, 2, 3]
lst[1:1] = ['a', 'b'] # Insert at position 1
# Result: [1, 'a', 'b', 2, 3]
📊 Range Function Complete Guide
range(start=0, stop, step=1)
range(stop)
From 0 to stop-1
range(start, stop)
From start to stop-1
range(start, stop, step)
From start to stop-1, incrementing by step
# Basic usage
list(range(5)) # [0, 1, 2, 3, 4]
list(range(2, 7)) # [2, 3, 4, 5, 6]
list(range(0, 10, 2)) # [0, 2, 4, 6, 8]
# Negative ranges
list(range(10, 0, -1)) # [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
list(range(-5, 5, 2)) # [-5, -3, -1, 1, 3]
# Edge cases
list(range(5, 10, 0)) # ValueError: step cannot be 0
list(range(1.5, 5.5)) # TypeError: float not allowed
Critical Errors:
• step=0 → ValueError
• Float arguments → TypeError
• Large step beyond range → Single element or empty
🔢 Enumerate Function
enumerate(iterable, start=0)
iterable
Any iterable object
start
Starting number (can be negative!)
# Basic usage
list(enumerate(['a', 'b', 'c'])) # [(0, 'a'), (1, 'b'), (2, 'c')]
list(enumerate(['a', 'b', 'c'], start=1)) # [(1, 'a'), (2, 'b'), (3, 'c')]
# Negative start (perfectly valid!)
list(enumerate(['a', 'b', 'c'], start=-1)) # [(-1, 'a'), (0, 'b'), (1, 'c')]
# On strings
list(enumerate("hi")) # [(0, 'h'), (1, 'i')]
# Empty iterable
list(enumerate([])) # []
🤝 Zip Function
# Basic usage - stops at shortest
list(zip([1, 2, 3], ['a', 'b'])) # [(1, 'a'), (2, 'b')]
# Empty iterable makes result empty
list(zip([1, 2, 3], [], ['a', 'b'])) # []
# Single argument creates single-element tuples
list(zip([1, 2, 3])) # [(1,), (2,), (3,)]
# Zip with strings
list(zip("abc", "12")) # [('a', '1'), ('b', '2')]
# Unzipping with * operator
pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
list(zip(*pairs)) # [(1, 2, 3), ('a', 'b', 'c')]
Key Behavior: zip() always stops at the shortest iterable. Empty iterable = empty result.
🔄 Loop Patterns
# for-else: else runs if NO break occurred
for i in range(3):
if i == 5: # Never true
break
else:
print("Completed") # This prints
# while-else: else runs if NO break occurred
x = 0
while x < 3:
x += 1
if x == 2:
break
else:
x = 100 # Doesn't run because of break
print(x) # Prints 2
# Nested loops with continue
result = []
for i in range(2):
for j in range(3):
if i == j:
continue # Skip when i equals j
result.append((i, j))
# Result: [(0, 1), (0, 2), (1, 0), (1, 2)]
🔢 Type Conversion Functions
| Function |
Parameters |
Example |
Result |
int() |
x=0, string, base=10 |
int("ff", 16) |
255 |
float() |
x=0.0 |
float("inf") |
inf |
str() |
object='', encoding, errors |
str(b'hi', 'utf-8') |
'hi' |
bool() |
x=False |
bool([]) |
False |
# int() with different bases
int("1010", 2) # 10 (binary)
int("ff", 16) # 255 (hexadecimal)
int("77", 8) # 63 (octal)
int("123", 10) # 123 (decimal - default)
# Number format functions
bin(10) # "0b1010"
oct(64) # "0o100"
hex(255) # "0xff"
# Character conversions
chr(65) # "A"
ord('A') # 65
ord('A') - ord('a') # -32 (case difference)
📐 Mathematical Functions
# Basic math
abs(-5) # 5
round(2.675, 2) # 2.67 (banker's rounding!)
divmod(17, 5) # (3, 2) - quotient and remainder
# Advanced power function
pow(2, 3) # 8
pow(2, 3, 5) # 3 (equivalent to (2**3) % 5)
# Min/Max with options
max([1, 2, 3]) # 3
max([], default=42) # 42 (no ValueError!)
min('apple', 'banana', key=len) # 'apple'
# Sum with start value
sum([1, 2, 3]) # 6
sum([1, 2, 3], 10) # 16 (starts with 10)
Banker's Rounding: round(2.675, 2) = 2.67 (rounds to nearest even). This catches many people!
🔍 Filtering and Transformation
# filter() removes falsy values when function is None
list(filter(None, [0, 1, 2, '', 'hello', False])) # [1, 2, 'hello']
# map() applies function to each element
list(map(str, [1, 2, 3])) # ['1', '2', '3']
list(map(lambda x, y: x + y, [1, 2], [10, 20])) # [11, 22]
# sorted() with key function
sorted(['apple', 'pie', 'a'], key=len) # ['a', 'pie', 'apple']
sorted([3, 1, 4], reverse=True) # [4, 3, 1]
# any() and all()
any([False, False, True]) # True (at least one truthy)
all([True, True, False]) # False (not all truthy)
any([]) # False (empty is falsy)
all([]) # True (vacuously true)
🔎 Inspection Functions
# Object inspection
len([1, 2, 3]) # 3
type(123) #
isinstance(123, (int, float)) # True
id([1, 2, 3]) # Memory address
# Attribute handling
hasattr(obj, 'attr') # True/False
getattr(obj, 'attr', 'default') # Get with default
setattr(obj, 'attr', value) # Set attribute
delattr(obj, 'attr') # Delete attribute
# Directory and variables
dir(str) # List of string methods
vars(obj) # obj.__dict__
callable(print) # True
hash((1, 2)) # Hash value (tuples are hashable)
📝 String and Formatting Functions
# Advanced string methods
"hello".center(10, '-') # "--hello---"
"hello".ljust(10, '.') # "hello....."
"hello".rjust(10, '.') # ".....hello"
# String case and validation
"hello".capitalize() # "Hello"
"hello world".title() # "Hello World"
"HELLO".lower() # "hello"
"hello".upper() # "HELLO"
# Format function with advanced patterns
format(42, '08b') # "00101010" (8-digit binary)
format(3.14159, '.2f') # "3.14"
format("hello", '^10') # " hello " (centered)
# Advanced f-string formatting
name, width = "Python", 10
f"{name:^{width}}" # " Python " (centered with variable width)
🔧 Advanced Built-ins
# Memory and binary operations
memoryview(b"hello")[1] # 101 (byte value of 'e')
frozenset([1, 2, 2, 3]) # frozenset({1, 2, 3})
# Evaluation functions (use carefully!)
eval("2 + 3") # 5
exec("x = 5") # Executes code
# Iterator functions
iter([1, 2, 3]) # Creates iterator
next(iterator) # Gets next value
next(iterator, 'default') # Next with default
reversed([1, 2, 3]) # Reverse iterator
# Slice objects
s = slice(1, 5, 2)
[0, 1, 2, 3, 4, 5][s] # [1, 3] (slice as object)
🎯 Indexing Behavior
# Basic indexing
lst = [1, 2, 3]
lst[0] # 1 (first element)
lst[-1] # 3 (last element)
lst[-3] # 1 (first element via negative)
# Beyond bounds behavior
lst[5] # IndexError: list index out of range
lst[-5] # IndexError: list index out of range
# Slicing is forgiving
lst[10:20] # [] (empty, no error)
lst[-10:-5] # [] (empty, no error)
lst[::100] # [1] (large step, gets first element)
Critical Difference: Direct indexing beyond bounds = IndexError. Slicing beyond bounds = empty list.
⭐ Star Expression Unpacking
# Basic star unpacking
a, *b, c = [1, 2, 3, 4, 5]
# a = 1, b = [2, 3, 4], c = 5
# Multiple star expressions in assignment = SyntaxError
*a, *b = [1, 2, 3, 4] # SyntaxError!
# Nested unpacking
(a, b), (c, d) = [(1, 2), (3, 4)]
# a = 1, b = 2, c = 3, d = 4
# Insufficient values
a, b, c = [1, 2] # ValueError: not enough values to unpack
# Star in function calls
def func(a, b, c): return a + b + c
func(*[1, 2, 3]) # 6
func(**{'a': 1, 'b': 2, 'c': 3}) # 6
Rules:
• Only ONE star expression per assignment
• Star captures middle/remaining elements
• Must have enough values for non-star variables
🔗 Assignment Patterns
# Chained assignment creates shared references
a = b = [1, 2, 3]
a.append(4)
print(b) # [1, 2, 3, 4] - same object!
# Tuple assignment (simultaneous)
a, b = 10, 20
a, b = b, a + b # a = 20, b = 30 (Fibonacci step)
# Swapping
x, y = y, x
# Complex assignment combinations
a, (b, c) = 1, (2, 3) # a = 1, b = 2, c = 3
🎨 Advanced Slicing Patterns
# Complex negative slicing
lst = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
lst[-3::-2] # [7, 5, 3, 1] (from index 7, step -2)
lst[4:1:-1] # [4, 3, 2] (backwards from 4 to 2)
# String slicing
s = "abcdefgh"
s[1:7:2] # "bdf" (every 2nd char from index 1-6)
# Slice with None (equivalent to defaults)
lst[None:None:None] # Same as lst[:] - full copy
# Empty ranges
lst[1:1] # [] (empty slice)
lst[5:2] # [] (start > stop with positive step)
🔄 Method Resolution Order (MRO)
class A:
def method(self): return "A"
class B:
def method(self): return "B"
class C(A, B): # Inherits from A first, then B
pass
C().method() # "A" - follows MRO: C -> A -> B
print(C.__mro__) # Shows the method resolution order
MRO Rule: Python searches left-to-right in inheritance list. First match wins.
📊 Class vs Instance Variables
class MyClass:
class_var = [] # Shared among ALL instances!
a = MyClass()
b = MyClass()
a.class_var.append(1)
print(b.class_var) # [1] - they share the same list!
# Correct way for instance variables
class MyClass:
def __init__(self):
self.instance_var = [] # Each instance gets its own
Mutable Class Variables: Lists, dicts, sets at class level are shared. Often a bug source!
🎯 Properties and Decorators
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self): # Read-only computed property
return 3.14159 * self._radius ** 2
# Usage
c = Circle(5)
print(c.area) # 78.53975 (computed)
c.radius = 10 # Uses setter
# c.area = 100 # AttributeError - no setter
🎭 Magic Methods (Dunder Methods)
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __str__(self): # For print()
return f"Point({self.x}, {self.y})"
def __repr__(self): # For debugging
return f"Point(x={self.x}, y={self.y})"
def __add__(self, other): # For + operator
return Point(self.x + other.x, self.y + other.y)
def __eq__(self, other): # For == comparison
return self.x == other.x and self.y == other.y
def __len__(self): # For len()
return int((self.x**2 + self.y**2)**0.5)
# Usage
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1) # "Point(1, 2)"
p3 = p1 + p2 # Point(4, 6)
print(len(p1)) # 2
🏛️ Inheritance and Super
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Call parent constructor
self.breed = breed
def speak(self): # Override parent method
return f"{self.name} barks"
# Usage
dog = Dog("Buddy", "Golden Retriever")
print(dog.speak()) # "Buddy barks"
🔄 Generators
# Generator function
def countdown(n):
while n > 0:
yield n
n -= 1
# Generator is consumed once
gen = countdown(3)
print(next(gen)) # 3
print(list(gen)) # [2, 1] - remaining values
# Generator with send()
def echo_gen():
value = None
while True:
value = yield value # Receive and send back
print(f"Received: {value}")
gen = echo_gen()
next(gen) # Prime the generator
gen.send("hello") # Prints: Received: hello
# Generator expressions (memory efficient)
squares = (x**2 for x in range(1000000)) # No list created
sum_squares = sum(x**2 for x in range(1000))
Generator Key Points:
• Consumed once (like iterators)
• Memory efficient for large datasets
• Use send() to send values into generator
🎭 Decorators
# Basic decorator
def timer(func):
import time
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
@timer
def slow_function():
import time
time.sleep(1)
slow_function() # Prints timing info
# Decorator with parameters
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet():
print("Hello!")
greet() # Prints "Hello!" three times
🏠 Context Managers
# Basic context manager
class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
return False # Don't suppress exceptions
# Usage
with FileManager("test.txt", "w") as f:
f.write("Hello, World!")
# Exception suppression
class ErrorHandler:
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ValueError:
print("Handled ValueError")
return True # Suppress the exception
return False
with ErrorHandler():
raise ValueError("This will be caught")
📝 Comprehensions Advanced
# List comprehension with condition
result = [x if x > 0 else 0 for x in range(-3, 4)]
# [0, 0, 0, 0, 1, 2, 3]
# Nested comprehensions
matrix = [[i*j for j in range(1, 4)] for i in range(1, 4)]
# [[1, 2, 3], [2, 4, 6], [3, 6, 9]]
# Multiple iterables
pairs = [(x, y) for x in [1, 2] for y in [10, 20]]
# [(1, 10), (1, 20), (2, 10), (2, 20)]
# Dictionary comprehension with filtering
word_lengths = {word: len(word) for word in ['apple', 'pie', 'a'] if len(word) > 1}
# {'apple': 5, 'pie': 3}
# Set comprehension
unique_squares = {x**2 for x in range(-5, 6)}
# {0, 1, 4, 9, 16, 25}
🎯 Try/Except/Else/Finally
try:
result = risky_operation()
except SpecificError:
result = "error_value"
else:
# Runs only if NO exception occurred
result = "success_value"
finally:
# ALWAYS runs, even if there's a return in try/except
cleanup()
# Finally overrides return values!
def tricky_function():
try:
return "try"
finally:
return "finally" # This wins!
print(tricky_function()) # "finally"
Finally Override: Return statements in finally block override returns from try/except blocks!
🔗 Exception Chaining
# Chain exceptions with 'from'
try:
1 / 0
except ZeroDivisionError as e:
raise ValueError("Math error") from e
# Suppress chaining with 'from None'
try:
1 / 0
except ZeroDivisionError:
raise ValueError("New error") from None # No traceback chain
# Access chained exceptions
try:
# ... chained exception code ...
except ValueError as e:
print(e.__cause__) # Original exception
print(e.__context__) # Implicit chaining
🎭 Multiple Exception Types
# Catch multiple exception types
try:
risky_operation()
except (ValueError, TypeError) as e:
print(f"Got {type(e).__name__}: {e}")
# Different handling for different exceptions
try:
process_data()
except ValueError:
print("Data validation error")
except TypeError:
print("Type mismatch error")
except Exception as e:
print(f"Unexpected error: {e}")
# Re-raising exceptions
try:
operation()
except ValueError:
log_error("Data error occurred")
raise # Re-raise the same exception
🎯 Walrus Operator (Python 3.8+)
# Assignment expressions
if (n := len(items)) > 5:
print(f"Too many items: {n}")
# In comprehensions
results = [result for item in items if (result := process(item)) > 0]
# In while loops
while (line := input("Enter command: ")) != "quit":
process_command(line)
Walrus Usage: Assign and use in same expression. Great for avoiding duplicate calculations.
🔄 Dictionary Union Operator (Python 3.9+)
# Dictionary union with |
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
merged = dict1 | dict2 # {'a': 1, 'b': 3, 'c': 4}
# In-place union with |=
dict1 |= dict2 # Modifies dict1
# Older equivalent
merged = {**dict1, **dict2} # Same result, works in older Python
🔢 Floating Point Precision
# Floating point precision issues
print(0.1 + 0.2 == 0.3) # False!
print(0.1 + 0.2) # 0.30000000000000004
# Solutions
from decimal import Decimal
print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3')) # True
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True
Floating Point: Never use == for float comparisons. Use math.isclose() or Decimal for precision.
🧠 Memory and Identity
# Identity vs equality
a = [1, 2, 3]
b = a # Same object
c = a.copy() # Different object, same content
print(a is b) # True (same identity)
print(a is c) # False (different identity)
print(a == c) # True (same content)
print(id(a) == id(b)) # True
print(id(a) == id(c)) # False
# Small integer caching
x = 256
y = 256
print(x is y) # True (cached)
x = 257
y = 257
print(x is y) # False (not cached) - implementation detail!
🎨 Advanced Slicing Edge Cases
# Complex slice deletion
lst = [1, 2, 3, 4, 5, 6]
del lst[::2] # Deletes indices 0, 2, 4
print(lst) # [2, 4, 6]
# Overlapping slice assignment
lst = [1, 2, 3, 4, 5]
lst[1:4] = lst[2:5] # Replace with overlapping slice
print(lst) # [1, 3, 4, 5]
# Backward slicing edge cases
lst = [1, 2, 3, 4, 5]
print(lst[1:4:-1]) # [] (empty - start < stop with negative step)
print(lst[4:1:-1]) # [5, 4, 3] (backwards from 4 to 2)
🔧 Mutable Default Arguments
# The classic Python gotcha
def append_to_list(item, target_list=[]): # DANGEROUS!
target_list.append(item)
return target_list
print(append_to_list(1)) # [1]
print(append_to_list(2)) # [1, 2] - same list!
# Correct approach
def append_to_list(item, target_list=None):
if target_list is None:
target_list = []
target_list.append(item)
return target_list
Mutable Defaults: Default arguments are evaluated once at function definition. Mutable defaults = shared state!
🎯 Quiz Success Tips
Focus Areas for High Scores:
• Master slice assignment size rules (basic vs step slicing)
• Know all built-in function parameters (especially range, enumerate, zip)
• Understand MRO and class variable sharing
• Practice star expression unpacking patterns
• Remember edge cases: banker's rounding, floating point precision
• Know when operations return iterators vs lists
• Understand exception handling execution order