GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing learning-zone.github.io (truncated) — Python Basics python-basicsPython Basics
Table of Contents
Python for AI & Data Science
# 1. IntroductionQ. What is Python?Python is a popular programming language. It was created by Guido van Rossum, and released in 1991. It is used for: web development (server-side), software development, mathematics, system scripting. Q. What can Python do?Python can be used on a server to create web applications. Python can be used alongside software to create workflows. Python can connect to database systems. It can also read and modify files. Python can be used to handle big data and perform complex mathematics. Python can be used for rapid prototyping, or for production-ready software development. Q. When is using Python the “right choice” for a project?Python is a high-level, interpreted, interactive and object-oriented scripting language. Python is designed to be highly readable. It uses English keywords frequently where as other languages use punctuation, and it has fewer syntactical constructions than other languages. Python is a high-level general-purpose programming language that can be applied to many different classes of problems. The language comes with a large standard library that covers areas such as string processing like regular expressions, Unicode, calculating differences between files, Internet protocols like HTTP, FTP, SMTP, XML-RPC, POP, IMAP, CGI programming, software engineering like unit testing, logging, profiling, parsing Python code, and operating system interfaces like system calls, file systems, TCP/IP sockets. Although likes and dislikes are highly personal, a developer who is “worth his or her salt” will highlight features of the Python language that are generally considered advantageous (which also helps answer the question of what Python is “particularly good for”. Some of the more common valid answers to this question include:
Q. What are some drawbacks of the Python language?The two most common valid answers to this question are:
Q. We know Python is all the rage these days. But to be truly accepting of a great technology, you must know its pitfalls as well?Of course. To be truly yourself, you must be accepting of your flaws. Only then can you move forward to work on them. Python has its flaws too: Python's interpreted nature imposes a speed penalty on it. While Python is great for a lot of things, it is weak in mobile computing, and in browsers. Being dynamically-typed, Python uses duck-typing (If it looks like a duck, it must be a duck). This can raise runtime errors. Python has underdeveloped database access layers. This renders it a less-than-perfect choice for huge database applications. And even after these pitfalls, of course. Being easy makes it addictive. Once a Python-coder, always a Python coder. So while it has problems, it is also a wonderful tool for a lot of things. Q. What are the key differences between Python 2 and 3?
API changes:
Q. What are some key differences to bear in mind when coding in Python vs. Java?Disclaimer #1. The differences between Java and Python are numerous and would likely be a topic worthy of its own (lengthy) post. Below is just a brief sampling of some key differences between the two languages. Disclaimer #2. The intent here is not to launch into a religious battle over the merits of Python vs. Java (as much fun as that might be!). Rather, the question is really just geared at seeing how well the developer understands some practical differences between the two languages. The list below therefore deliberately avoids discussing the arguable advantages of Python over Java from a programming productivity perspective.
Q. What are the key features of Python?If it makes for an introductory language to programming, Python must mean something. These are its qualities:
Q. How is a .pyc file different from a .py file?While both files hold bytecode, .pyc is the compiled version of a Python file. It has platform-independent bytecode. Hence, we can execute it on any platform that supports the .pyc format. Python automatically generates it to improve performance(in terms of load time, not speed). Q. How does the Python version numbering scheme work?Python versions are numbered A.B.C or A.B. A is the major version number. It is only incremented for major changes in the language. B is the minor version number, incremented for less earth-shattering changes. C is the micro-level. It is incremented for each bug fix release. Not all releases are bug fix releases. In the run-up to a new major release, ‘A’ series of development releases are made denoted as alpha, beta, or release candidate. Alphas are early releases in which interfaces aren't finalized yet; it's not unexpected to see an interface change between two alpha releases. Betas are more stable, preserving existing interfaces but possibly adding new modules, and release candidates are frozen, making no changes except as needed to fix critical bugs. Alpha, beta and release candidate versions have an additional suffix. The suffix for an alpha version is “aN” for some small number N, The suffix for a beta version is “bN” for some small number N, And the suffix for a release candidate version is “cN” for some small number N. In other words, all versions labeled 2.0aN precede the versions labeled 2.0bN, which precede versions labeled 2.0cN, and those precede 2.0. You may also find version numbers with a “+” suffix, e.g. “2.2+”. These are unreleased versions, built directly from the subversion trunk. In practice, after a final minor release is made, the subversion trunk is incremented to the next minor version, which becomes the “a0” version, e.g. “2.4a0”. Q. What Are The Implementation In Python Program?Python program can be implemented by two ways
In Interactive mode python command shell is required. It is available in installation of python cell. In Interactive mode is not suitable for developing the projects & Applications Interactive mode is used for predefined function and programs. Example: X, Y is not find. Interactive mode is unfit for looping purpose. Interactive Mode:
The concept of submitting one by one python statements explicitly in the python interpreter is known as “Interactive Mode”
In Order to submit the one by one python statements explicitly to the python interpreter we use python command line shell.
Python command line shell is present in python software
We can open the python command line shell by executing python command on command prompt or terminal
Example:
c:/users/mindmajix>python
Batch Mode: In Order to develop the python files we use editors or IDE's
Different editors are notepad, notepad++, edit+,nano, VI, gedil and so on. Example: Save the file in D drive mindmajix python folder with the demo.py Open command prompt and execute following commands: Python D:/mindmajix python/Demo.py 3000 -1000 2000.000 Save another method if we correctly set the path D: D:/>cd “mindmajix python” D:/mindmajix Python>python Demo.py Q. What Are Applications of Python?Applications of Python
Q. Mention five benefits of using Python?
Q. Is Python interpreted or compiled?As noted in Why Are There So Many Pythons?, this is, frankly, a bit of a trick question in that it is malformed. Python itself is nothing more than an interface definition (as is true with any language specification) of which there are multiple implementations. Accordingly, the question of whether “Python” is interpreted or compiled does not apply to the Python language itself; rather, it applies to each specific implementation of the Python specification. Further complicating the answer to this question is the fact that, in the case of CPython (the most common Python implementation), the answer really is “sort of both”. Specifically, with CPython, code is first compiled and then interpreted. More precisely, it is not precompiled to native machine code, but rather to bytecode. While machine code is certainly faster, bytecode is more portable and secure. The bytecode is then interpreted in the case of CPython (or both interpreted and compiled to optimized machine code at runtime in the case of PyPy). Q. What are some alternative implementations to CPython? When and why might you use them?One of the more prominent alternative implementations is Jython, a Python implementation written in Java that utilizes the Java Virtual Machine (JVM). While CPython produces bytecode to run on the CPython VM, Jython produces Java bytecode to run on the JVM. Another is IronPython, written in C# and targeting the .NET stack. IronPython runs on Microsoft's Common Language Runtime (CLR). As also pointed out in Why Are There So Many Pythons?, it is entirely possible to survive without ever touching a non-CPython implementation of Python, but there are advantages to be had from switching, most of which are dependent on your technology stack. Another noteworthy alternative implementation is PyPy whose key features include:
Q. Is Python a compiled language?Python is both compiled and interpreted. CPython (the reference implementation) compiles source code to platform-independent bytecode ( Alternatives:
Use Case: A fintech platform migrated a CPU-bound risk calculation engine from CPython to PyPy, reducing processing time from 45s to 9s with zero code changes, leveraging PyPy's JIT compilation. Q. Why Python instead of Scala on Spark when Scala has better performance?
The performance gap has significantly narrowed because:
Use Case: A data science team at an e-commerce company chose PySpark over Scala Spark to reuse their existing Q. What is Python?Python is a high-level, interpreted, dynamically-typed, multi-paradigm, general-purpose programming language created by Guido van Rossum and first released in 1991. It emphasizes code readability and developer productivity through its clean syntax and extensive standard library. Key characteristics:
Use Case: Python serves as the lingua franca of data science and ML engineering — TensorFlow, PyTorch, scikit-learn, pandas, and Apache Spark all offer Python-first APIs, making Python the single language teams use across data exploration, model training, API serving, and infrastructure automation. Q. Describe some features of Python.
Use Case: Python's multi-paradigm nature lets a data engineering team write functional pipeline transformations ( Q. How does Python execute code?Python's execution pipeline has six stages: Key points:
Use Case: A Django application with hundreds of modules benefits from Q. What is the difference between Python 2.7+ and Python 3?Python 3 (released 2008, Python 2 EOL January 2020) introduced breaking changes to fix long-standing design flaws. Python 2 is no longer supported.
Use Case: Migrating a legacy Python 2 data pipeline to Python 3 fixes silent data corruption where Q. Is Python compiled? If yes, how? If not, how?Both — Python is compiled to bytecode, then interpreted. It is not compiled to native machine code (unless using PyPy's JIT or tools like Cython/Nuitka). Execution pipeline: Comparison:
Use Case: A deployment pipeline precompiles all Q. What does dynamically / duck typed mean?Dynamic typing: Variable types are checked at runtime, not compile time. A name can be rebound to any type at any time — the name carries no type information itself; the object has a type. Duck typing: “If it walks like a duck and quacks like a duck, it's a duck.” Python does not check what class an object is — it only checks whether the object has the required method or attribute. Formal class inheritance is not required. Dynamic vs Static typing:
Use Case: A data pipeline's Q. When would I not use Python?Python is an excellent general-purpose language but has known limitations where other tools are more appropriate:
When Python is still used despite these limitations:
Use Case: A fintech company uses Python for data analytics and ML model training but writes the order matching engine in C++ with microsecond latency, exposing Python bindings via Q. What is DRY, and how can I apply it through OOP or FP?DRY (Don't Repeat Yourself): Every piece of knowledge or logic should have a single authoritative representation in the codebase. Duplication means two places to update and two places to introduce bugs. DRY strategies:
Use Case: A microservices platform DRYs up HTTP retry logic with a single Q. When would I use Python?Python excels in domains where developer productivity, readability, and ecosystem richness matter more than raw execution speed:
Use Case: A startup uses Python end-to-end: Airflow for ETL orchestration, FastAPI for the REST backend, pandas for analytics, pytest for CI testing, and boto3 for AWS infrastructure — a single language team covers the full stack. Q. Give examples of Python frameworks.
Use Case: A company's production stack uses Django for the customer-facing portal (ORM, admin, auth), FastAPI for internal microservices (high-throughput async endpoints), Airflow for nightly ETL jobs, and Celery for async email/notification tasks — all Python, all interoperating. Q. How is Python interpreted?Python's interpretation process has distinct stages. “Interpreted” means source code is never compiled to native machine code — it is instead compiled to bytecode which a software VM (the CPython interpreter loop) executes instruction by instruction. Key CPython details:
Use Case: A platform engineering team sets Q. What is Dynamic Typing?Dynamic typing means type information is attached to objects at runtime, not to variables at compile time. A variable name is just a label that can be rebound to any object of any type at any point. Benefits: Flexibility, rapid prototyping, less boilerplate. Use Case: A data science notebook exploits dynamic typing to reuse the same variable name across exploration phases ( Q. Justify this statement: Everything is an object in Python.In Python, every value — integers, strings, functions, classes, modules, Implication: Because everything is an object, Python enables true first-class functions, decorators, metaclasses, introspection, and dynamic behaviour that would require special syntax in other languages. Use Case: A plugin framework stores user-provided callables (functions, class instances with Q. What is the difference between Python and Java?
When to choose Python: Prototyping, ML/DS, scripting, small-to-medium web APIs. Use Case: A fintech firm writes its ML risk models in Python (pandas, scikit-learn) but the core transaction processing engine in Java (Spring Boot) for JIT performance and strict type safety — connecting them via a REST/gRPC boundary. # 2. Data TypesQ. How do I convert a number to a string?To convert, e.g., the number 144 to the string ‘144’, use the built-in function str(). If you want a hexadecimal or octal representation, use the built-in functions hex() or oct(). For fancy formatting, use the % operator on strings, e.g. “%04d” % 144 yields ‘0144’ and “%.3f” % (1/3.0) yields ‘0.333’. See the library reference manual for details. Q. Does Python support strongly for regular expressions?Yes, Python Supports Regular Expressions Well. Regular expressions (called REs, or regexes, or regex patterns) are essentially a tiny, highly specialized programming language embedded inside Python and made available through the re module. Using this little language, you specify the rules for the set of possible strings that you want to match; this set might contain English sentences, or e-mail addresses, or TeX commands, or anything you like. You can then ask questions such as “Does this string match the pattern?”, or “Is there a match for the pattern anywhere in this string?”. You can also use REs to modify a string or to split it apart in various ways. Regular expression patterns are compiled into a series of bytecodes which are then executed by a matching engine written in C. For advanced use, it may be necessary to pay careful attention to how the engine will execute a given RE, and write the RE in a certain way in order to produce bytecode that runs faster. Optimization isn't covered in this document, because it requires that you have a good understanding of the matching engine's internals. Q. How do you perform pattern matching in Python? Explain.Regular Expressions/REs/ regexes enable us to specify expressions that can match specific “parts” of a given string. For instance, we can define a regular expression to match a single character or a digit, a telephone number, or an email address, etc. The Python's “re” module provides regular expression patterns and was introduce from later versions of Python 2.5. “re” module is providing methods for search text strings, or replacing text strings along with methods for splitting text strings based on the pattern defined. Q. Write a regular expression that will accept an email id. Use the
|
| Method | In-place | Returns new list | Time | Space |
|---|---|---|---|---|
list.reverse() |
Yes | No (None) |
O(n) | O(1) |
reversed() |
No | Iterator | O(1) | O(1) |
Slice [::-1] |
No | New list | O(n) | O(n) |
from typing import TypeVar
T = TypeVar("T")
numbers: list[int] = [1, 2, 3, 4, 5]
# Method 1: In-place mutation (most memory-efficient for large lists)
numbers.reverse()
print(numbers) # [5, 4, 3, 2, 1]
# Method 2: Slice — creates a new reversed copy
original = [1, 2, 3, 4, 5]
reversed_copy = original[::-1]
print(reversed_copy) # [5, 4, 3, 2, 1]
print(original) # [1, 2, 3, 4, 5] (unchanged)
# Method 3: reversed() iterator — lazy, best for one-pass iteration
for item in reversed([1, 2, 3, 4, 5]):
print(item, end=" ") # 5 4 3 2 1
Use Case: In a data pipeline processing time-series records, reversed() is used to iterate over rows newest-first without copying the entire dataset into memory — critical when processing millions of records.
The optimal approach uses heapq.merge() which runs in O(n + m) time and O(1) extra space (lazy iterator). A manual two-pointer approach is also O(n + m) but builds the result eagerly.
import heapq
from typing import Iterator
# Method 1: heapq.merge — lazy O(1) space iterator (preferred for large lists)
list_a: list[int] = [1, 3, 5, 7, 9]
list_b: list[int] = [2, 4, 6, 8, 10]
merged: Iterator[int] = heapq.merge(list_a, list_b)
print(list(merged)) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Method 2: Two-pointer — O(n + m) time, O(n + m) space
def merge_sorted(a: list[int], b: list[int]) -> list[int]:
result: list[int] = []
i = j = 0
while i < len(a) and j < len(b):
if a[i] <= b[j]:
result.append(a[i])
i += 1
else:
result.append(b[j])
j += 1
result.extend(a[i:])
result.extend(b[j:])
return result
print(merge_sorted([1, 3, 5], [2, 4, 6])) # [1, 2, 3, 4, 5, 6]
Use Case: In a distributed search engine, each shard returns a sorted list of ranked results. heapq.merge() lazily merges all shard results to produce a globally sorted feed without loading all results into memory.
Python's built-in and standard-library data structures cover most algorithmic needs:
| Structure | Module | Ordered | Mutable | Unique | Avg Lookup |
|---|---|---|---|---|---|
list |
built-in | Yes | Yes | No | O(n) |
tuple |
built-in | Yes | No | No | O(n) |
dict |
built-in | Yes (3.7+) | Yes | Keys only | O(1) |
set |
built-in | No | Yes | Yes | O(1) |
frozenset |
built-in | No | No | Yes | O(1) |
deque |
collections |
Yes | Yes | No | O(1) ends |
defaultdict |
collections |
Yes | Yes | Keys only | O(1) |
OrderedDict |
collections |
Yes | Yes | Keys only | O(1) |
namedtuple |
collections |
Yes | No | No | O(1) |
Counter |
collections |
Yes | Yes | Keys only | O(1) |
heapq (min-heap) |
heapq |
Partial | Yes | No | O(log n) push/pop |
from collections import deque, defaultdict, Counter, namedtuple
import heapq
# deque — O(1) append/pop from both ends (unlike list's O(n) left operations)
queue: deque[int] = deque([1, 2, 3])
queue.appendleft(0)
queue.append(4)
print(queue) # deque([0, 1, 2, 3, 4])
# Counter — frequency map
word_freq = Counter(["python", "java", "python", "rust", "python"])
print(word_freq.most_common(2)) # [('python', 3), ('java', 1)]
# namedtuple — immutable record with named fields (memory-efficient vs class)
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y) # 3 4
# heapq — min-heap for priority queue
tasks: list[tuple[int, str]] = []
heapq.heappush(tasks, (2, "medium priority"))
heapq.heappush(tasks, (1, "high priority"))
heapq.heappush(tasks, (3, "low priority"))
print(heapq.heappop(tasks)) # (1, 'high priority')
Use Case: A real-time leaderboard service uses heapq to maintain the top-K scores efficiently (O(log K) per insert), Counter for aggregating player event frequencies, and deque as a circular buffer for a sliding-window activity feed.
A list comprehension is a concise, readable syntax for creating a new list by applying an expression to each item in an iterable, with an optional filter condition. It is faster than an equivalent for loop because the iteration happens internally in C (CPython).
Syntax: [expression for item in iterable if condition]
# Basic: square numbers 0–9
squares: list[int] = [x ** 2 for x in range(10)]
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# With filter: even squares only
even_squares: list[int] = [x ** 2 for x in range(10) if x % 2 == 0]
print(even_squares) # [0, 4, 16, 36, 64]
# Nested: flatten a 2D matrix
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat: list[int] = [cell for row in matrix for cell in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# String processing: extract valid emails from raw strings
raw_data = ["[email protected]", "not-an-email", "[email protected]", "bad@@test"]
valid_emails: list[str] = [
addr.strip().lower()
for addr in raw_data
if "@" in addr and addr.count("@") == 1
]
print(valid_emails) # ['[email protected]', '[email protected]']
# Dict comprehension variant
word_lengths: dict[str, int] = {word: len(word) for word in ["python", "java", "rust"]}
print(word_lengths) # {'python': 6, 'java': 4, 'rust': 4}
# Set comprehension — unique first letters
first_letters: set[str] = {word[0] for word in ["apple", "ant", "banana", "avocado"]}
print(first_letters) # {'a', 'b'}
Performance: List comprehensions are typically 1.5–2× faster than equivalent for loops because they avoid repeated attribute lookups for list.append.
Use Case: A data ingestion pipeline uses list comprehensions to transform and filter 100K raw JSON records into clean domain objects in a single readable expression, replacing a 15-line for loop and reducing code review friction.
from typing import Any
# Method 1: List literal (most common)
numbers: list[int] = [1, 2, 3, 4, 5]
# Method 2: list() constructor
from_tuple: list[int] = list((10, 20, 30))
from_range: list[int] = list(range(5)) # [0, 1, 2, 3, 4]
from_str: list[str] = list("abc") # ['a', 'b', 'c']
# Method 3: List comprehension (preferred for transformations)
squares: list[int] = [x ** 2 for x in range(10)]
# Method 4: Multiplication (shallow copy — beware with mutable elements)
zeros: list[int] = [0] * 5 # [0, 0, 0, 0, 0]
# Method 5: Empty list and append
result: list[Any] = []
result.append("a")
result.extend([1, 2])
print(result) # ['a', 1, 2]
# Key operations and complexity
lst = [3, 1, 4, 1, 5, 9, 2, 6]
lst.sort() # O(n log n) in-place Timsort
print(lst[2:5]) # O(k) slicing — [3, 4, 5]
print(4 in lst) # O(n) linear search
lst.insert(0, 0) # O(n) — shifts all elements
lst.pop() # O(1) — remove from end
lst.pop(0) # O(n) — remove from front (use deque instead)
Use Case: A recommendation engine collects candidate items into a list, sorts them by score with a custom key function (list.sort(key=lambda x: x.score, reverse=True)), and slices the top-K results — leveraging Python's Timsort which is O(n log n) worst-case but O(n) for nearly-sorted data.
A list comprehension creates a list by evaluating an expression for each element of an iterable, with an optional filter. It is more readable and ~1.5–2× faster than an equivalent for loop because the loop runs in C internally and avoids list.append attribute lookup overhead.
Use it when: creating a new list from a transformation/filter in one readable line.
Avoid it when: logic is complex (multiple conditions, side effects, nested 3+ levels).
# Why faster: no append() call overhead per iteration
import timeit
# For loop version
def squares_loop(n: int) -> list[int]:
result = []
for x in range(n):
result.append(x ** 2)
return result
# List comprehension version
def squares_comp(n: int) -> list[int]:
return [x ** 2 for x in range(n)]
t_loop = timeit.timeit(lambda: squares_loop(1000), number=10000)
t_comp = timeit.timeit(lambda: squares_comp(1000), number=10000)
print(f"Loop: {t_loop:.3f}s Comprehension: {t_comp:.3f}s")
# Comprehension is typically ~40% faster
# Real-world example: parse and clean CSV-like data
raw_records = [
" alice , 30 , engineer ",
" bob , 25 , designer ",
" , , ", # invalid row
]
cleaned: list[dict[str, str]] = [
{"name": parts[0], "age": parts[1], "role": parts[2]}
for record in raw_records
if (parts := [f.strip() for f in record.split(",")]) and all(parts)
]
print(cleaned)
# [{'name': 'alice', 'age': '30', 'role': 'engineer'}, {'name': 'bob', ...}]
Use Case: An ETL pipeline processes 500K JSON records from S3 with a single list comprehension to extract, validate, and normalize fields, making the transformation self-documenting and testable as a pure function.
Comprehensions are concise, readable expressions that create a new collection by applying a transformation and optional filter to an iterable — all in one line. They run faster than equivalent for loops because the loop body executes in C.
# List comprehension: [expression for item in iterable if condition]
squares = [x ** 2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
cleaned = [s.strip().lower() for s in [" Alice ", "BOB", " Charlie"]]
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
print(cleaned) # ['alice', 'bob', 'charlie']
# Nested list comprehension (flatten 2D → 1D)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [val for row in matrix for val in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Dict comprehension: {key_expr: val_expr for item in iterable if condition}
word_lengths = {word: len(word) for word in ["python", "java", "rust"]}
inverted = {v: k for k, v in {"a": 1, "b": 2, "c": 3}.items()}
print(word_lengths) # {'python': 6, 'java': 4, 'rust': 4}
print(inverted) # {1: 'a', 2: 'b', 3: 'c'}
# Set comprehension (unique values)
unique_lengths = {len(w) for w in ["hi", "hello", "hey", "howdy"]}
print(unique_lengths) # {2, 5}
# Generator expression (lazy — use when you don't need a list)
total = sum(x ** 2 for x in range(1_000_000)) # O(1) memory
# When to AVOID comprehensions
# BAD — multiple conditions + side effects: use a regular for loop
# result = [
# (process(item), log(item)) # side effect inside comprehension
# for item in items
# if validate(item) and item.active and item.score > threshold
# ]
Performance tip: List comprehensions are ~1.3–2× faster than for+append loops.
Use Case: A Kafka consumer normalizes incoming JSON messages with a dict comprehension: {k.lower().replace("-", "_"): str(v).strip() for k, v in raw.items()} — transforming keys and values in one readable expression before schema validation.
Python has three main ways to create array-like structures, each with different trade-offs:
# 1. Built-in list — flexible, heterogeneous, dynamic size
lst: list[int] = [1, 2, 3, 4, 5]
lst.append(6)
print(lst) # [1, 2, 3, 4, 5, 6]
# 2. array module — typed, memory-efficient, homogeneous (like C arrays)
import array
int_arr = array.array("i", [1, 2, 3, 4, 5]) # 'i' = signed int
dbl_arr = array.array("d", [1.1, 2.2, 3.3]) # 'd' = double
print(int_arr) # array('i', [1, 2, 3, 4, 5])
print(int_arr.itemsize) # 4 bytes per element (vs 28 for Python int)
int_arr.append(6)
print(int_arr.tolist()) # [1, 2, 3, 4, 5, 6]
# 3. numpy array — vectorized math, multi-dimensional, C-speed
import numpy as np
np_arr = np.array([1, 2, 3, 4, 5], dtype=np.int32)
print(np_arr * 2) # [2 4 6 8 10] — element-wise, no loop needed
print(np_arr.mean()) # 3.0
print(np_arr.sum()) # 15
# 2D array (matrix)
matrix = np.zeros((3, 4), dtype=np.float64) # 3 rows × 4 cols
print(matrix.shape) # (3, 4)
# numpy operations (no Python loop)
a = np.arange(1_000_000, dtype=np.float64)
b = np.arange(1_000_000, dtype=np.float64)
c = a + b # vectorized — ~100× faster than list + for loop
# Type codes for array.array
# 'b' = signed char, 'B' = unsigned char
# 'h' = short, 'i' = int, 'l' = long
# 'f' = float, 'd' = double
list |
array.array |
numpy.ndarray |
|
|---|---|---|---|
| Typed | No | Yes | Yes |
| Multi-dim | No | No | Yes |
| Math ops | Manual | Manual | Vectorized |
| Memory | High | Low | Low–Medium |
| Speed | Slow | Medium | Fast (C) |
Use Case: A signal processing service stores 10M ADC samples in numpy.ndarray(dtype=np.int16) — 20 MB instead of ~280 MB for a Python list — and applies FFT with np.fft.fft() in milliseconds using BLAS/LAPACK internally.
list / tuple / set / dict?| Structure | Ordered | Mutable | Duplicates | Key-value | Use when |
|---|---|---|---|---|---|
list |
Yes | Yes | Yes | No | Ordered, changeable sequence |
tuple |
Yes | No | Yes | No | Fixed record, dict key, return multiple values |
set |
No | Yes | No | No | Membership test, deduplication, set operations |
frozenset |
No | No | No | No | Immutable set, hashable (dict key) |
dict |
Yes (3.7+) | Yes | Keys: No | Yes | Key→value mapping, O(1) lookup |
# list — ordered, mutable sequence
pipeline_steps: list[str] = ["extract", "transform", "load"]
pipeline_steps.append("validate") # add at end — O(1)
# tuple — immutable record; can be a dict key
point = (3.0, 4.0) # coordinates — immutable pair
rgb = (255, 128, 0) # fixed-size record
cache: dict[tuple, int] = {point: 1} # tuple as dict key (hashable!)
# Multiple return values (naturally a tuple)
def min_max(values: list[float]) -> tuple[float, float]:
return min(values), max(values)
lo, hi = min_max([3, 1, 4, 1, 5, 9])
print(lo, hi) # 1 9
# set — fast membership, deduplication, set algebra
seen: set[str] = set()
visited: set[str] = {"a", "b", "c"}
new: set[str] = {"b", "c", "d", "e"}
print("a" in visited) # True — O(1) hash lookup
print(visited & new) # {'b', 'c'} — intersection
print(visited | new) # {'a', 'b', 'c', 'd', 'e'} — union
print(visited - new) # {'a'} — difference
# Remove duplicates from a list (preserving order via dict trick)
dupes = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
unique = list(dict.fromkeys(dupes))
print(unique) # [3, 1, 4, 5, 9, 2, 6]
# dict — named fields, O(1) lookup
user: dict[str, object] = {"id": 1, "name": "Alice", "active": True}
print(user.get("role", "viewer")) # safe get with default
Use Case: A URL deduplication stage uses a set to track seen URLs (O(1) lookup), stores extracted data as named dict records, passes immutable (source_id, record_id) tuples as task identifiers to Celery (hashable, safe to use as cache keys), and collects ordered results in a list.
Use list comprehensions when:
for+append)Avoid list comprehensions when:
import timeit
data = range(1000)
# GOOD: simple, readable transformation
squares = [x ** 2 for x in data]
# GOOD: filter + transform
even_squares = [x ** 2 for x in data if x % 2 == 0]
# GOOD: replace map/filter
cleaned = [s.strip().lower() for s in [" Alice ", "BOB", " Carol "]]
# GOOD: flatten one level
matrix = [[1, 2], [3, 4], [5, 6]]
flat = [v for row in matrix for v in row]
# AVOID: deeply nested — unreadable
# BAD:
nested = [
(i, j, k)
for i in range(3)
for j in range(3)
for k in range(3)
if i != j != k
]
# Use itertools.product() or a regular for loop instead
# AVOID: side effects inside comprehension
import logging
# BAD:
# results = [process(item) for item in items if log_and_validate(item)]
# Use a for loop when you need side effects
# AVOID: large intermediate list you'll only iterate once — use generator
# BAD:
total = sum([x ** 2 for x in range(1_000_000)]) # builds full list in memory
# GOOD:
total = sum(x ** 2 for x in range(1_000_000)) # generator expression, O(1) memory
# Performance proof
t_loop = timeit.timeit(
"r=[]; [r.append(x**2) for x in range(1000)]", number=10000
)
t_comp = timeit.timeit(
"[x**2 for x in range(1000)]", number=10000
)
print(f"loop: {t_loop:.3f}s comp: {t_comp:.3f}s")
# comprehension is ~40% faster
Rule of thumb: If it fits naturally on one line and is easy to read, use a comprehension. If you need if/elif/else chains, multiple side effects, or more than two nested for clauses — write a regular for loop.
Use Case: A data pipeline uses list comprehensions for field extraction ([r["id"] for r in rows]) but switches to explicit for loops for multi-step transformations that require logging, error handling, and conditional branching per record.
tuple and a list? Where will you use each?list |
tuple |
|
|---|---|---|
| Mutability | Mutable | Immutable |
| Syntax | [1, 2, 3] |
(1, 2, 3) |
| Memory | Larger (over-allocates) | Smaller (fixed) |
| Hashable | No | Yes (if all elements are) |
| Can be dict key | No | Yes |
| Iteration speed | Slightly slower | Slightly faster |
| Intended semantics | Homogeneous sequence | Heterogeneous record |
import sys
# Memory comparison
lst = list(range(10))
tpl = tuple(range(10))
print(sys.getsizeof(lst)) # 184 bytes (over-allocated)
print(sys.getsizeof(tpl)) # 136 bytes (exact)
# list — ordered, changeable sequence (homogeneous items)
tasks: list[str] = ["extract", "transform", "load"]
tasks.append("validate")
tasks[0] = "fetch"
# tuple — fixed record (heterogeneous, positional meaning)
point = (3.0, 4.0) # 2D coordinate — positions have meaning
person = ("Alice", 30, "SWE") # (name, age, role) — named positions
# Named tuple for readability
from collections import namedtuple
from typing import NamedTuple
class Employee(NamedTuple):
name: str
age: int
role: str
emp = Employee("Bob", 28, "DevOps")
print(emp.name) # Bob
print(emp[0]) # Bob (also index-accessible)
# Tuple as dict key — list cannot be
location_cache: dict[tuple[float, float], str] = {}
location_cache[(51.5074, -0.1278)] = "London"
location_cache[(48.8566, 2.3522)] = "Paris"
# Tuple unpacking — clean multiple assignment
x, y, z = (1, 2, 3)
first, *rest = (10, 20, 30, 40)
print(first, rest) # 10 [20, 30, 40]
# Swap using tuple
a, b = 1, 2
a, b = b, a # (b, a) tuple — no temp variable needed
print(a, b) # 2 1
When to use:
list — when you need to add, remove, or sort items (shopping cart, pipeline steps)tuple — when the structure is fixed (coordinates, database rows, function return values, dict keys)Use Case: A geospatial service stores cached reverse-geocoding results in dict[tuple[float, float], str] — using (lat, lon) tuples as hashable keys — and returns coordinate results as tuple[float, float] to signal immutability to callers.
*args, **kwargs ?In cases when we don't know how many arguments will be passed to a function, like when we want to pass a list or a tuple of values, we use *args.
def func(*args):
for i in args:
print(i)
func(3,2,1,4,7)
3
2
1
4
7
**kwargs takes keyword arguments when we don't know how many there will be:
def func(**kwargs):
for i in kwargs:
print(i,kwargs[i])
func(a=1,b=2,c=7)
a.1
b.2
c.7
The words args and kwargs are a convention, and we can use anything in their place.
Collect the arguments using the * and ** specifier in the function's parameter list; this gives you the positional arguments as a tuple and the keyword arguments as a dictionary. You can then pass these arguments when calling another function by using * and ** :
def f(x, *tup, **kwargs):
kwargs['width']='14.3c'
g(x, *tup, **kwargs)
In the unlikely case that you care about Python versions older than 2.0, use ‘apply’:
def f(x, *tup, **kwargs):
kwargs['width']='14.3c'
apply(g, (x,)+tup, kwargs)
You have two choices: you can use nested scopes or you can use callable objects. For example, suppose you wanted to define linear(a,b) which returns a function f(x) that computes the value a*x+b.
Using nested scopes:
def linear(a,b):
def result(x):
return a*x + b
return result
Or
using a callable object:
class linear:
def __init__(self, a, b):
self.a, self.b = a,b
def __call__(self, x):
return self.a * x + self.b
In both cases:
taxes = linear(0.3,2) gives a callable object where
taxes(10e6) == 0.3 * 10e6 + 2.
The callable object approach has the disadvantage that it is a bit slower and results in slightly longer code. However, note that a collection of callables can share their signature via inheritance:
class exponential(linear):
__init__ inherited
def __call__(self, x):
return self.a * (x ** self.b)
Object can encapsulate state for several methods:
class counter:
value = 0
def set(self, x):
self.value = x
def up(self):
self.value=self.value+1
def down(self):
self.value=self.value-1
count = counter()
inc, dec, reset = count.up, count.down, count.set
Here inc(), dec() and reset() act like functions which share the same counting variable.
The help() function displays the documentation string and help for its argument.
import copy
help(copy.copy)
Help on function copy in module copy: copy(x) Shallow copy operation on arbitrary Python objects. See the module's doc string for more info. The dir() function displays all the members of an object(any kind).
dir(copy.copy)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
In Python, a closure is a function object that has access to variables in its enclosing lexical scope, even when the function is called outside that scope. In other words, a closure allows a function to remember and access the values of the variables in the environment where it was created, even if those variables are no longer in scope when the function is called.
Closures are created when a nested function references a value from its enclosing function. The enclosing function returns the nested function, which maintains a reference to the enclosed value. The enclosed value is stored in the closure, which is attached to the nested function object.
Here's an example of a closure in Python:
def outer_function(x):
def inner_function(y):
return x + y
return inner_function
closure = outer_function(10)
result = closure(5)
print(result) # Output: 15
def A(x):
def B():
print(x)
return B
A(7)()
7
Since Python does not have a concept of private variables, it is a convention to use leading underscores to declare a variable private. This is why we mustn't do that to variables we do not want to make private.
There are two ways to do this:
a,b,c=3,4,5 #This assigns 3, 4, and 5 to a, b, and c resp.
a = b = c =3 #This assigns 3 to a, b, and c
When we want to execute a sequence of statements, we can give it a name. Let's define a function to take two numbers and return the greater number.
def greater(a,b):
return a is a>b else b
greater(3,3.5)
3.5
You can create your own function or use one of Python's many built-in functions.
Recursion is a programming technique in which a function calls itself to solve a problem. The idea is to break down a complex problem into smaller, simpler problems and solve them in a recursive manner until the base case is reached. The base case is a condition that stops the recursion and returns a value.
A common example of a recursive function is the factorial function, which calculates the product of all positive integers up to a given number. Here's an example of a recursive factorial function in Python:
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
def facto(n):
if n==1: return 1
return n*facto(n-1)
facto(4)
24
One of the less common functions with beginners, zip() returns an iterator of tuples.
list(zip(['a','b','c'],[1,2,3]))
[(‘a’, 1), (‘b’, 2), (‘c’, 3)]
Here, it pairs items from the two lists, and creates tuples with those. But it doesn't have to be lists.
list(zip(('a','b','c'),(1,2,3)))
[(‘a’, 1), (‘b’, 2), (‘c’, 3)]
Ans. The function Counter() from the module ‘collections’. It counts the number of occurrences of the elements of a container.
from collections import Counter
Counter([1,3,2,1,4,2,1,3,1])
Counter({1: 4, 3: 2, 2: 2, 4: 1})
Python provides us with a range of ways and methods to work with a Counter. Read Python Counter.
Ans. First, let's discuss the local and global scope. By example, a variable defined inside a function is local to that function. Another variable defined outside any other scope is global to the function.
Suppose we have nested functions. We can read a variable in an enclosing scope from inside he inner function, but cannot make a change to it. For that, we must declare it nonlocal inside the function. First, let's see this without the nonlocal keyword.
def outer():
a=7
def inner():
print(a)
inner()
outer()
7
def outer():
a=7
def inner():
print(a)
a+=1
print(a)
inner()
outer()
Traceback (most recent call last):
File “<pyshell#462>”, line 1, in
So now, let's try doing this with the ‘nonlocal’ keyword:
def outer():
a=7
def inner():
nonlocal a
print(a)
a+=1
print(a)
inner()
outer()
Ans. Like we saw in the previous question, the global keyword lets us deal with, inside any scope, the global version of a variable.
The problem:
a=7
def func():
print(a)
a+=1
print(a)
The solution:
a=7
def func():
global a
print(a)
a+=1
print(a)
func()
Local variables: If a variable is assigned a new value anywhere within the function's body, it's assumed to be local.
Global variables: Those variables that are only referenced inside a function are implicitly global.
To share global variables across modules within a single program, create a special module. Import the config module in all modules of your application. The module will be available as a global variable across modules.
In Python, you can return multiple values from a function by packing them into a tuple or a list. Here's an example using a tuple:
def get_user_info(user_id):
# ... do some work to retrieve user info ...
name = "John Doe"
email = "[email protected]"
age = 35
return name, email, age
# call the function and unpack the returned values
user_name, user_email, user_age = get_user_info(123)
# print the values
print(user_name) # Output: John Doe
print(user_email) # Output: [email protected]
print(user_age) # Output: 35
In this example, the get_user_info function returns three values (name, email, and age) as a tuple. When the function is called, the returned tuple is unpacked into individual variables (user_name, user_email, and user_age) using tuple unpacking syntax. The variables can then be used as needed.
You can also return multiple values as a list or any other iterable. For example:
def get_user_info(user_id):
# ... do some work to retrieve user info ...
name = "John Doe"
email = "[email protected]"
age = 35
return [name, email, age]
# call the function and unpack the returned values
user_info = get_user_info(123)
user_name, user_email, user_age = user_info
# print the values
print(user_name) # Output: John Doe
print(user_email) # Output: [email protected]
print(user_age) # Output: 35
In this example, the get_user_info function returns a list instead of a tuple, and the returned values are unpacked into variables using list unpacking syntax.
In Python, the fastest way to swap the values bound to two variables is to use tuple unpacking, like this:
a, b = b, a
In this code, the values of a and b are swapped by creating a tuple of the values in reverse order ((b, a)) and unpacking them into the variables a and b. The assignment is performed simultaneously, which makes this method faster than using a temporary variable, which requires an additional assignment.
For example:
a = 5
b = 10
# swap the values using tuple unpacking
a, b = b, a
print(a) # Output: 10
print(b) # Output: 5
In this example, the values of a and b are swapped using tuple unpacking, and the final output shows that the values have indeed been swapped.
return statement?Yes. In Python, every function returns a value. If no return statement is present (or return is used without a value), the function implicitly returns None.
def greet(name: str) -> None:
print(f"Hello, {name}")
result = greet("Alice")
print(result) # Output: None
print(type(result)) # Output: <class 'NoneType'>
Use Case: This behaviour is relied upon in API validation helpers — a function that validates input and raises exceptions on failure is called for its side effects, not its return value. The caller checks if validate(data) is None to confirm successful validation.
Python supports four argument types: positional, default (optional), *args (variadic positional), and **kwargs (variadic keyword).
from typing import Any
# Default arguments — make parameter optional
def greet(name: str, greeting: str = "Hello", punct: str = "!") -> str:
return f"{greeting}, {name}{punct}"
print(greet("Alice")) # Hello, Alice!
print(greet("Bob", greeting="Hi")) # Hi, Bob!
print(greet("Eve", "Hey", ".")) # Hey, Eve.
# *args — variable number of positional arguments
def total(*values: float) -> float:
return sum(values)
print(total(1, 2, 3, 4)) # 10.0
# **kwargs — variable number of keyword arguments
def build_url(base: str, **params: Any) -> str:
query = "&".join(f"{k}={v}" for k, v in params.items())
return f"{base}?{query}" if query else base
print(build_url("https://api.example.com", page=1, limit=20, sort="desc"))
# https://api.example.com?page=1&limit=20&sort=desc
# Combining all types (order matters: positional, *args, keyword-only, **kwargs)
def create_record(
table: str, # positional
*fields: str, # variadic positional
schema: str = "public", # keyword-only (after *)
**metadata: Any, # variadic keyword
) -> dict:
return {"table": table, "fields": fields, "schema": schema, **metadata}
rec = create_record("users", "id", "name", "email", schema="app", version=2)
print(rec)
# {'table': 'users', 'fields': ('id', 'name', 'email'), 'schema': 'app', 'version': 2}
# Unpacking when calling
def add(a: int, b: int, c: int) -> int:
return a + b + c
args = [1, 2, 3]
kwargs = {"a": 10, "b": 20, "c": 30}
print(add(*args)) # 6 — unpack list as positional args
print(add(**kwargs)) # 60 — unpack dict as keyword args
Use Case: A REST client wrapper accepts **query_params so callers can pass any API-supported filter without updating the function signature for every new parameter — future-proofing the interface while keeping calls readable.
enumerate() in Python?enumerate() wraps an iterable and yields (index, value) pairs, eliminating the need for a manual counter variable. It is O(1) memory (returns a lazy iterator) and supports a start parameter to control the starting index.
fruits = ["apple", "banana", "cherry", "date"]
# Without enumerate — error-prone manual counter
for i in range(len(fruits)):
print(i, fruits[i])
# With enumerate — Pythonic, cleaner
for i, fruit in enumerate(fruits):
print(i, fruit)
# 0 apple
# 1 banana
# 2 cherry
# 3 date
# Custom start index
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}")
# 1. apple
# 2. banana
# 3. cherry
# 4. date
# Building indexed structures
index_map = {fruit: i for i, fruit in enumerate(fruits)}
print(index_map) # {'apple': 0, 'banana': 1, 'cherry': 2, 'date': 3}
# Tracking position while modifying (safe pattern)
data = [3, -1, 4, -1, 5, 9, -2]
for i, val in enumerate(data):
if val < 0:
data[i] = 0 # replace negatives in-place using index
print(data) # [3, 0, 4, 0, 5, 9, 0]
# Works with any iterable
for line_no, line in enumerate(open("README.md"), start=1):
if "TODO" in line:
print(f"Line {line_no}: {line.rstrip()}")
break
Use Case: A CSV validator uses enumerate(rows, start=2) (header is row 1) to report validation errors with their exact spreadsheet line number: "Row 47: missing required field 'email'" — without any manual counter.
Namespace: A mapping (dictionary) from names to objects. Examples: locals(), globals(), a class body, a module, builtins.
Scope: The region of source code where a namespace is directly accessible without qualification. Python resolves names using the LEGB rule: Local → Enclosing → Global → Built-in.
# Global namespace
MODULE_CONST = "global"
def outer():
# Enclosing namespace
enclosing_var = "enclosing"
def inner():
# Local namespace
local_var = "local"
# LEGB lookup order:
print(local_var) # L — local
print(enclosing_var) # E — enclosing
print(MODULE_CONST) # G — global
print(len) # B — built-in
inner()
outer()
# global and nonlocal keywords modify outer scopes
counter = 0
def increment():
global counter # modify global, not create a local shadow
counter += 1
increment()
print(counter) # 1
def make_counter():
count = 0
def inc():
nonlocal count # modify enclosing scope variable
count += 1
return count
return inc
c = make_counter()
print(c()) # 1
print(c()) # 2
# Inspect namespaces at runtime
def show_namespaces():
x = 10
print("locals:", locals())
print("globals keys:", list(globals().keys())[:5])
show_namespaces()
# Class body has its own namespace (but NOT a scope for nested functions)
class Config:
DEBUG = True
DB_URL = "postgres://..."
# Inside a method, Config.DEBUG — must qualify with class name
Key distinction:
Use Case: A plugin system uses globals() to dynamically register handler functions by name at import time, and locals() to safely evaluate template expressions in a restricted namespace — separating the framework's global namespace from per-render local context.
from typing import Optional
# 1. Basic function
def greet(name: str) -> str:
return f"Hello, {name}!"
print(greet("Alice")) # Hello, Alice!
# 2. Default arguments
def power(base: float, exponent: int = 2) -> float:
return base ** exponent
print(power(3)) # 9.0
print(power(3, 3)) # 27.0
# 3. *args and **kwargs
def summarize(*args: float, label: str = "Total", **meta) -> dict:
return {"label": label, "sum": sum(args), "count": len(args), **meta}
print(summarize(1, 2, 3, label="Score", unit="pts"))
# {'label': 'Score', 'sum': 6, 'count': 3, 'unit': 'pts'}
# 4. Return multiple values (returns a tuple)
def min_max(values: list[float]) -> tuple[float, float]:
return min(values), max(values)
lo, hi = min_max([3, 1, 4, 1, 5, 9])
print(lo, hi) # 1 9
# 5. Lambda — anonymous single-expression function
square = lambda x: x ** 2
print(square(7)) # 49
# 6. Nested functions and closures
def make_multiplier(factor: int):
def multiply(x: float) -> float:
return x * factor # closes over 'factor'
return multiply
triple = make_multiplier(3)
print(triple(10)) # 30.0
# 7. Decorator
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@log_call
def add(a: int, b: int) -> int:
return a + b
add(2, 3)
# Calling add
# add returned 5
# 8. Generator function
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
Use Case: A data transformation library exposes its API entirely as plain functions with type annotations, enabling mypy static analysis, easy unit testing (assert transform(input) == expected), and simple composition via functools.partial and compose.
A function returns a value using the return statement. Python allows returning multiple values (as a tuple), returning early, and using generators to yield values lazily.
# 1. Single return value
def square(n: float) -> float:
return n ** 2
# 2. Multiple return values — actually returns a tuple
def divmod_custom(a: int, b: int) -> tuple[int, int]:
return a // b, a % b # quotient and remainder
quotient, remainder = divmod_custom(17, 5)
print(quotient, remainder) # 3, 2
raw = divmod_custom(17, 5)
print(type(raw)) # <class 'tuple'>
# 3. Early return (guard clauses)
def safe_sqrt(n: float) -> float | None:
if n < 0:
return None # early exit
return n ** 0.5
# 4. Return a function (closure / higher-order)
def make_adder(n: int):
def adder(x: int) -> int:
return x + n
return adder # return a function object
add5 = make_adder(5)
print(add5(10)) # 15
# 5. Return a complex object
from dataclasses import dataclass
@dataclass
class ParseResult:
success: bool
value: float | None
error: str | None = None
def parse_number(s: str) -> ParseResult:
try:
return ParseResult(success=True, value=float(s))
except ValueError as e:
return ParseResult(success=False, value=None, error=str(e))
r = parse_number("3.14")
print(r) # ParseResult(success=True, value=3.14, error=None)
# 6. Generator — yield instead of return (lazy values)
def even_range(n: int):
for i in range(n):
if i % 2 == 0:
yield i # yields one at a time, not all at once
print(list(even_range(10))) # [0, 2, 4, 6, 8]
Use Case: An API handler returns a dataclass Result(ok, data, error) from every service call — callers pattern-match on result.ok rather than catching exceptions, making error handling explicit and the happy path obvious.
return statement?A function without a return statement (or with a bare return) implicitly returns None. This is by design — Python always returns an object; None is the sentinel for “no meaningful value”.
def greet(name: str) -> None:
print(f"Hello, {name}!")
result = greet("Alice") # prints "Hello, Alice!"
print(result) # None
print(type(result)) # <class 'NoneType'>
# Bare return also returns None
def process(value: int) -> int | None:
if value < 0:
return # same as: return None
return value * 2
print(process(-1)) # None
print(process(5)) # 10
# Common pitfall — accidentally using the return value of a mutating method
lst = [3, 1, 2]
sorted_lst = lst.sort() # list.sort() returns None (in-place)
print(sorted_lst) # None ← BUG! should use sorted(lst)
# Fix:
sorted_lst = sorted(lst) # sorted() returns a new list
print(sorted_lst) # [1, 2, 3]
# Type checkers (mypy) will warn if you assign None to a typed variable
def no_return():
x = 1 + 1 # computes but discards
val = no_return()
# mypy: error: "no_return" does not return a value (it only ever returns None)
# None check pattern
def find(items: list, target) -> int | None:
for i, item in enumerate(items):
if item == target:
return i
# falls off end — returns None
idx = find([1, 2, 3], 99)
if idx is None:
print("Not found")
Use Case: A data validation function returns None on success and a ValidationError (or error string) on failure — callers check if error := validate(data): handle(error), making the “no return = success” contract explicit.
A function which doesn't contain any name is known as a anonymous function lambda function, Lambda function we can assign to the variable & we can call the lambda function through the variable.
Syntax:
Lambda arguments:expression
It is a single expression anonymous function often used as inline function. A lambda form in python does not have statements as it is used to make new function object and then return them at runtime.
The lambda operator is used to create anonymous functions. It is mostly used in cases where one wishes to pass functions as parameters. or assign them to variable names.
When we want a function with a single expression, we can define it anonymously. A lambda expression may take input and returns a value. To define the above function as a lambda expression, we type the following code in the interpreter:
(lambda a,b:a if a>b else b)(3,3.5)
3.5
Here, a and b are the inputs.
a if a > b else b is the expression to return.
The arguments are 3 and 3.5.
It is possible to not have any inputs here.
(lambda :print("Hi"))()
Hi
Example:
myfunction = lambda x:x*x
a = myfunction(10)
print(a)
Output: 100
Lambdas evaluates at run time and these do not need statements Lambda is a anonymous function, which does not have a name and no fixed number of arguments. Represented by keyword lambda followed by statement.
Ex:
add = lambda a,b: a+b
add(2,3)
output:
5
Map function executes the function given as the first argument on all the elements of the iterable given as the second argument. If the function given takes in more than 1 arguments, then many iterables are given. #Follow the link to know more similar functions
Python supports methods (called iterators in Python3), such as filter(), map(), and reduce(), that are very useful when you need to iterate over the items in a list, create a dictionary, or extract a subset of a list.
filter() – enables you to extract a subset of values based on conditional logic.map() – it is a built-in function that applies the function to each item in an iterable.reduce() – repeatedly performs a pair-wise reduction on a sequence until a single value is computed.Sometimes, when we want to iterate over a list, a few methods come in handy.
filter(): Filter lets us filter in some values based on conditional logic.
list(filter(lambda x:x>5,range(8)))
Ans: [6, 7]
map(): Map applies a function to every element in an iterable.
list(map(lambda x:x**2,range(8)))
Ans: [0, 1, 4, 9, 16, 25, 36, 49]
reduce(): Reduce repeatedly reduces a sequence pair-wise until we reach a single value
from functools import reduce
reduce(lambda x,y:x-y,[1,2,3,4,5])
Ans: -13
map(), reduce(), and filter()?All three are higher-order functions from functional programming that operate on iterables:
| Function | Operation | Returns | Python 3 |
|---|---|---|---|
map(f, iterable) |
Apply f to each element |
map iterator |
Built-in |
filter(pred, iterable) |
Keep elements where pred is truthy |
filter iterator |
Built-in |
reduce(f, iterable) |
Fold elements into single value using f |
single value | functools.reduce |
from functools import reduce
from typing import Callable
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# map — transform each element (lazy iterator)
doubled = list(map(lambda x: x * 2, numbers))
print(doubled) # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# filter — select elements matching predicate (lazy iterator)
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4, 6, 8, 10]
# reduce — aggregate to single value
total = reduce(lambda acc, x: acc + x, numbers, 0)
print(total) # 55
product = reduce(lambda acc, x: acc * x, numbers, 1)
print(product) # 3628800
# Chained pipeline (functional composition)
result = reduce(
lambda acc, x: acc + x,
filter(lambda x: x % 2 == 0,
map(lambda x: x ** 2, numbers))
)
print(result) # sum of squares of evens: 4+16+36+64+100 = 220
# Modern Pythonic equivalent (often more readable)
result2 = sum(x ** 2 for x in numbers if x % 2 == 0)
print(result2) # 220
Performance note: map() and filter() return lazy iterators in Python 3 — no intermediate list is allocated. reduce() is O(n) regardless.
Use Case: A data transformation service uses map() to normalize 1M user records (lowercase, strip), filter() to remove invalid entries, and reduce() to compute aggregate statistics — all in a single memory-efficient pipeline without materializing intermediate collections.
Functional programming (FP) treats computation as the evaluation of pure functions, avoiding shared state and mutable data. Python supports FP through map, filter, functools.reduce, closures, higher-order functions, and immutable data.
Why use it?
from functools import reduce, partial
from itertools import chain, starmap
from typing import Callable, TypeVar
T = TypeVar("T")
# 1. Pure functions — no side effects, same input → same output
def add(a: int, b: int) -> int:
return a + b
# 2. Higher-order functions
def compose(*funcs: Callable) -> Callable:
"""Compose functions right to left: compose(f, g)(x) == f(g(x))"""
return reduce(lambda f, g: lambda x: f(g(x)), funcs)
double = lambda x: x * 2
add_one = lambda x: x + 1
square = lambda x: x ** 2
transform = compose(double, add_one, square) # double(add_one(square(x)))
print(transform(3)) # double(add_one(9)) = double(10) = 20
# 3. Partial application
multiply = lambda x, y: x * y
triple = partial(multiply, 3)
print(list(map(triple, [1, 2, 3, 4]))) # [3, 6, 9, 12]
# 4. Functional pipeline using generators
orders = [
{"product": "A", "qty": 5, "price": 10.0, "active": True},
{"product": "B", "qty": 2, "price": 50.0, "active": False},
{"product": "C", "qty": 10, "price": 5.0, "active": True},
]
total_revenue = reduce(
lambda acc, order: acc + order["qty"] * order["price"],
filter(lambda o: o["active"], orders),
0.0,
)
print(f"Total: £{total_revenue:.2f}") # £100.00
Use Case: A billing engine implements its pricing logic as pure functions composed into a pipeline: apply_discount → add_tax → round_to_pence. Each step is independently unit-testable, and the whole pipeline can be parallelized with concurrent.futures.ProcessPoolExecutor safely because there is no shared mutable state.
map(), filter(), reduce(), and lambda.from functools import reduce
# lambda — anonymous function: lambda args: expression
square = lambda x: x ** 2
add = lambda x, y: x + y
print(square(5)) # 25
print(add(3, 4)) # 7
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# map(func, iterable) — apply func to each element → lazy iterator
doubled = list(map(lambda x: x * 2, numbers))
str_nums = list(map(str, numbers))
print(doubled) # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# filter(predicate, iterable) — keep elements where predicate is True → lazy
evens = list(filter(lambda x: x % 2 == 0, numbers))
positives = list(filter(None, [-1, 0, 2, 0, 5])) # None = truthiness check
print(evens) # [2, 4, 6, 8, 10]
print(positives) # [-1, 2, 5] wait — [-1 is truthy!] → [-1, 2, 5]
# reduce(func, iterable, initial) — fold to single value
total = reduce(lambda acc, x: acc + x, numbers, 0)
product = reduce(lambda acc, x: acc * x, numbers, 1)
maximum = reduce(lambda a, b: a if a > b else b, numbers)
print(total) # 55
print(product) # 3628800
print(maximum) # 10
# Chained functional pipeline
result = reduce(
lambda acc, x: acc + x, # sum
filter(lambda x: x % 2 == 0, # only evens
map(lambda x: x ** 2, numbers)) # squared
)
print(result) # 4+16+36+64+100 = 220
# Pythonic equivalent (often more readable)
result2 = sum(x ** 2 for x in numbers if x % 2 == 0)
print(result2) # 220
# When to use lambda vs def
# lambda: single expression, used inline (sort key, map/filter callbacks)
data = [{"name": "Bob", "age": 30}, {"name": "Alice", "age": 25}]
data.sort(key=lambda d: d["age"]) # lambda as sort key
print([d["name"] for d in data]) # ['Alice', 'Bob']
Use Case: A recommendation engine scores items with map() (compute scores), filters with filter() (remove below threshold), and reduces with reduce() (aggregate final ranking score) — three composable, testable steps replacing a monolithic scoring loop.
map(), filter(), and reduce() functions?All three are higher-order functions that operate on iterables without explicit for loops:
| Function | What it does | Returns | Module |
|---|---|---|---|
map(f, it) |
Apply f to every element |
lazy map iterator |
built-in |
filter(pred, it) |
Keep elements where pred is True |
lazy filter iterator |
built-in |
reduce(f, it, init) |
Fold elements into a single value | single value | functools |
from functools import reduce
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# map — transform each element
doubled = list(map(lambda x: x * 2, numbers))
as_str = list(map(str, numbers))
print(doubled) # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# filter — select elements matching predicate
evens = list(filter(lambda x: x % 2 == 0, numbers))
non_zero = list(filter(None, [0, 1, 2, 0, 3])) # None = truthiness
print(evens) # [2, 4, 6, 8, 10]
# reduce — accumulate to single value
total = reduce(lambda acc, x: acc + x, numbers, 0) # 55
product = reduce(lambda acc, x: acc * x, numbers, 1) # 3628800
maximum = reduce(lambda a, b: a if a > b else b, numbers) # 10
# Chained pipeline — sum of squares of even numbers
result = reduce(
lambda acc, x: acc + x,
filter(lambda x: x % 2 == 0,
map(lambda x: x ** 2, numbers))
)
print(result) # 220 (4+16+36+64+100)
# Pythonic equivalent (generator expression — often more readable)
result2 = sum(x ** 2 for x in numbers if x % 2 == 0)
print(result2) # 220
Use Case: A pricing engine uses map() to apply a discount function to all products, filter() to remove items below minimum margin, and reduce() to compute the cart total — three composable, independently testable steps.
A module can find out its own module name by looking at the predefined global variable __name__. If this has the value '__main__', the program is running as a script. Many modules that are usually used by importing them also provide a command-line interface or a self-test, and only execute this code after checking __name__:
def main():
print('Running test...')
if __name__ == '__main__':
main()
__import__('x.y.z')
returns Try: __import__('x.y.z').y.z
# For more realistic situations, you may have to do something like:
m = __import__(s)
for i in s.split(".")[1:]: m = getattr(m, i)
You can get a pointer to the module object as follows:
module = PyImport_ImportModule("");
If the module hasn't been imported yet (i.e. it is not yet present in sys.modules), this initializes the module; otherwise it simply returns the value of sys.modules[""]. Note that it doesn't enter the module into any namespace – it only ensures it has been initialized and is stored in sys.modules. You can then access the module's attributes (i.e. any name defined in the module) as follows: attr = PyObject_GetAttrString(module, “”); Calling PyObject_SetAttrString() to assign to variables in the module also works.
It overrides the any initialization from an inherited class and is called when the class is instantiated.
We know that a package may contain sub-packages and modules. A module is nothing but Python code.
To create a package of our own, we create a directory and create a file __init__.py in it. We leave it empty. Then, in that package, we create a module(s) with whatever code we want. For a detailed explanation with pictures, refer to Python Packages.
PYTHONSTARTUP − It contains the path of an initialization file containing Python source code. It is executed every time you start the interpreter. It is named as .pythonrc.py in Unix and it contains commands that load utilities or modify PYTHONPATH.
PYTHONCASEOK − It is used in Windows to instruct Python to find the first case-insensitive match in an import statement. Set this variable to any value to activate it.
PYTHONHOME − It is an alternative module search path. It is usually embedded in the PYTHONSTARTUP or PYTHONPATH directories to make switching module libraries easy.
PYTHONPATH − It has a role similar to PATH. This variable tells the Python interpreter where to locate the module files imported into a program. It should include the Python source library directory and the directories containing Python source code. PYTHONPATH is sometimes preset by the Python installer.
A module is a Python script that generally contains import statements, functions, classes and variable definitions, and Python runnable code and it “lives” file with a ‘.py’ extension. zip files and DLL files can also be modules.Inside the module, you can refer to the module name as a string that is stored in the global variable name .
Python provides libraries / modules with functions that enable you to manipulate text files and binary fileson file system. Using them you can create files, update their contents, copy, and delete files. The librariesare : os, os.path, and shutil.
Here,
os and os.path – modules include functions for accessing the filesystem
shutil – module enables you to copy and delete the files.
PYTHONPATH is the variable that tells the interpreter where to locate the module files imported into a program. Hence, it must include the Python source library directory and the directories containing Python source code. You can manually set PYTHONPATH, but usually, the Python installer will preset it.
If you can't find a source file for a module, it may be a built-in or dynamically loaded module implemented in C, C++ or other compiled language. In this case you may not have the source file or it may be something like mathmodule.c, somewhere in a C source directory (not on the Python Path). There are (at least) three kinds of modules in Python:
By default python interpreter search for the imported modules in the following locations:
Package is nothing but a folder or dictionary which represents collection of modules.
A package can also contain sub packages.
We can import the modules of the package by using package name.module name or name.subpackage name.module name
OS Module is a predefined module and which provides various functions and methods to perform the operating system related activities, such as creating the files, removing the files, creating the directories removing the directories, executing the operating system related commands, etc. Example:
import os
cwd=os.getwd()
print("1", cwd)
os.chdir("samples")
print("2", os.getcwd())
os.chdir(os.pardir)
print("3",os.getcwd())
Output:
In Python, module is the way to structure program. Each Python program file is a module, which imports other modules like objects and attributes.
The folder of Python program is a package of modules. A package can have modules or subfolders.
It depends on the use case. The two forms have different trade-offs:
| Form | Memory | Namespace | Lazy load | Recommendation |
|---|---|---|---|---|
import module |
Full module loaded | Explicit: module.func() |
No | Preferred for clarity |
from module import name |
Full module still loaded | Direct: func() |
No | Good for frequently used names |
from module import * |
Full module loaded | Pollutes namespace | No | Avoid (except __init__.py re-exports) |
# Recommended — explicit, avoids name collisions
import os
import json
import pathlib
path = pathlib.Path("/tmp/data.json")
data = json.loads(path.read_text())
# Good — importing specific names you use many times
from datetime import datetime, timezone
from collections import defaultdict
ts = datetime.now(tz=timezone.utc)
# Bad — wildcard import pollutes namespace and hides dependencies
# from os.path import * # now 'join', 'exists', 'split' are in scope — unclear origin
# Conditional / lazy import — for optional heavy dependencies
def load_data(path: str):
try:
import pandas as pd # deferred — only imported when needed
except ImportError:
raise ImportError("pandas is required: pip install pandas")
return pd.read_csv(path)
# Aliased import — standard for well-known libraries
import numpy as np
import pandas as pd
arr = np.array([1, 2, 3])
# Relative import inside a package
# from .models import User # import from sibling module
# from ..utils import helpers # import from parent package
Best practices:
import module keeps origin clear (os.path.join vs join)from module import name is fine for commonly used symbols (datetime, Path)from module import * in application codetensorflow, pandas) to avoid slow startup when the feature is optionalUse Case: A microservice with optional ML inference defers import torch inside the prediction function — the service starts in <200ms for health checks, and only pays the 4-second PyTorch load cost when an inference request actually arrives.
A module is any .py file. Python finds modules on sys.path. You import modules to reuse code across files.
# ── Creating a module ──────────────────────────────────────────────
# math_utils.py
PI = 3.14159265358979
def circle_area(r: float) -> float:
return PI * r * r
def circle_perimeter(r: float) -> float:
return 2 * PI * r
class Vector2D:
def __init__(self, x: float, y: float):
self.x, self.y = x, y
def magnitude(self) -> float:
return (self.x**2 + self.y**2) ** 0.5
# ── Importing ──────────────────────────────────────────────────────
import math_utils # whole module — use math_utils.PI
from math_utils import circle_area # single name
from math_utils import PI, Vector2D # multiple names
import math_utils as mu # alias
print(math_utils.circle_area(5))
print(circle_area(5)) # same result
print(mu.PI)
# ── Standard library modules ───────────────────────────────────────
import os, sys, json, pathlib, datetime, collections, itertools, functools
# os — OS interaction
print(os.getcwd())
print(os.environ.get("PATH", ""))
# pathlib — file system
p = pathlib.Path("README.md")
if p.exists():
print(p.stat().st_size, "bytes")
# json — serialization
data = {"key": "value", "num": 42}
print(json.dumps(data, indent=2))
# ── Package (directory with __init__.py) ──────────────────────────
# mypackage/
# __init__.py ← makes it a package
# database.py
# models/
# __init__.py
# user.py
# from mypackage import database
# from mypackage.models.user import User
# ── Inspecting modules ────────────────────────────────────────────
import math
print(dir(math)) # list all names in module
print(math.__file__) # path to module file
print(math.__doc__[:50]) # module docstring
Use Case: A microservice organises code into src/api/, src/services/, and src/repositories/ packages — each layer imports only what it needs via explicit relative imports, enforcing architectural boundaries and making unit testing straightforward.
If you're using new-style classes, use the built-in super() function:
class Derived(Base):
def meth (self):
super(Derived, self).meth()
If you're using classic classes: For a class definition such as
class Derived(Base): … you can call method meth() defined in Base (or one of Base's base classes) as Base.meth(self,arguments). Here, Base.meth is an unbound method, so you need to provide the self argument.
You could define an alias for the base class, assign the real base class to it before your class definition, and use the alias throughout your class. Then all you have to change is the value assigned to the alias. Incidentally, this trick is also handy if you want to decide dynamically (e.g. depending on availability of resources) which base class to use.
Example: BaseAlias = class Derived(BaseAlias): def meth(self): BaseAlias.meth(self).
When one class inherits from another, it is said to be the child/ derived/sub class inheriting from the parent/base/super class. It inherits/gains all members (attributes and methods). Inheritance lets us reuse our code, and also makes it easier to create and maintain applications.
Inheritance allows One class to gain all the members(say attributes and methods) of another class. Inheritance provides code reusability,makes it easier to create and maintain an application. The class from which we are inheriting is called super-class and the class that is inherited is called a derived/child class.
They are different types of inheritance supported by Python:
Single Inheritance – where a derived class acquires the members of a single super class.
OR
Single Inheritance- A class inherits from a single base class.
Multi-level inheritance – a derived class d1 in inherited from base class base1, and d2 is inherited from base2.
OR
Multilevel Inheritance- A class inherits from a base class, which in turn, inherits from another base class.
Hierarchical inheritance – from one base class you can inherit any number of child classes
OR
Hierarchical Inheritance- Multiple classes inherit from a single base class.
Multiple inheritance – a derived class is inherited from more than one base class.
OR
Multiple Inheritance- A class inherits from multiple base classes.
Hybrid Inheritance- Hybrid inheritance is a combination of two or more types of inheritance.
The concept of inheriting the properties from one class into multiple classes separately is known as hierarchical inheritance.
Example:
class x(object):
def m1(self):
print("in m1 of x")
class y(x):
def m2(self):
print("in m2 of y")
class z(x):
def m3(self):
print("in m3 of z")
y1=y()
y1.m1()
y1.m2()
a=y1.--hash--()
print(a)
z1=z()
z1.m1()
z1.m3()
b=z1.hash--()
print(b)
Output:
M m1 of X
In m2 of Y
2337815
In m1 of X
In m3 of Z
2099735
Ans. In our article on Multiple Inheritance in Python, we discussed Method Resolution Order (MRO). C does not contain its own version of func(). Since the interpreter searches in a left-to-right fashion, it finds the method in A, and does not go to look for it in B.
Ans. Here, we talk about three methods/functions- type(), isinstance() and issubclass().
a. type(): This tells us the type of object we're working with.
type(3)
<class ‘int’>
type(False)
<class ‘bool’>
type(lambda :print("Hi"))
<class ‘function’>
type(type)
<class 'type’>
b. isinstance()
This takes in two arguments- a value and a type. If the value is of the kind of the specified type, it returns True. Else, it returns False.
isinstance(3,int) True
isinstance((1),tuple) False
isinstance((1,),tuple) True
c. issubclass()
This takes two classes as arguments. If the first one inherits from the second, it returns True. Else, it returns False.
class A: pass
class B(A): pass
issubclass(B,A) True
issubclass(A,B) False
Yes. Python is Object Oriented Programming language. OOP is the programming paradigm based on classes and instances of those classes called objects. The features of OOP are: Encapsulation, Data Abstraction, Inheritance, Polymorphism.
Python does not provide interfaces like in Java. Abstract Base Class (ABC) and its feature are provided by the Python's “abc” module. Abstract Base Class is a mechanism for specifying what methods must be implemented by its implementation subclasses. The use of ABC’c provides a sort of “understanding” about methods and their expected behaviour. This module was made available from Python 2.7 version onwards.
Accessors and mutators are often called getters and setters in languages like “Java”. For example, if x is a property of a user-defined class, then the class would have methods called setX() and getX(). Python has an @property ‘decorator’ that allows you to ad getters and setters in order to access the attribute of the class.
Ans. What we call getters and setters in languages like Java, we term accessors and mutators in Python. In Java, if we have a user-defined class with a property ‘x’, we have methods like getX() and setX(). In Python, we have @property, which is syntactic sugar for property(). This lets us get and set variables without compromising on the conventions. For a detailed explanation on property, refer to Python property.
Ans. Suppose class B inherits from class A. Both have the method sayhello()- to each, their own version. B overrides the sayhello() of class A. So, when we create an object of class B, it calls the version that class B has.
class A:
def sayhello(self):
print("Hello, I'm A")
class B(A):
def sayhello(self):
print("Hello, I'm B")
a=A()
b=B()
a.sayhello()
Hello, I'm A
b.sayhello()
Hello, I’m B
Methods
Constructor
The concept of binding or grouping related data members along with its related functionalities is known as a Encapsulation.
| Aspect | Instance Variable | Class Variable |
|---|---|---|
| Defined in | __init__ (or any method) via self.x |
Class body, outside methods |
| Scope | Per-object — each instance has its own copy | Shared across all instances |
| Access | self.x or instance.x |
ClassName.x or self.x (via MRO) |
| Mutation | Affects only that instance | Affects all instances (if mutated on the class) |
class Employee:
company: str = "Acme Corp" # class variable — shared
_headcount: int = 0 # class variable — shared counter
def __init__(self, name: str, salary: float) -> None:
self.name = name # instance variable
self.salary = salary # instance variable
Employee._headcount += 1
@classmethod
def headcount(cls) -> int:
return cls._headcount
emp1 = Employee("Alice", 90_000)
emp2 = Employee("Bob", 75_000)
print(Employee.company) # "Acme Corp"
print(emp1.company) # "Acme Corp" — reads class variable via MRO
print(Employee.headcount()) # 2
# DANGER: assigning via instance SHADOWS the class variable, not mutates it
emp1.company = "New Corp"
print(emp1.company) # "New Corp" — instance now has its own copy
print(emp2.company) # "Acme Corp" — class variable untouched
print(Employee.company) # "Acme Corp"
# DANGER with mutable class variables — all instances share the SAME list
class BadExample:
items: list = [] # shared mutable default — antipattern!
a = BadExample()
b = BadExample()
a.items.append(1)
print(b.items) # [1] — b was mutated too!
# FIX: use None default and initialize in __init__
class GoodExample:
def __init__(self) -> None:
self.items: list = [] # each instance gets its own list
Use Case: A request-counting middleware uses a class variable RequestCounter.total as an atomic counter shared across all instances, while each instance tracks its own self.request_id — demonstrating the correct split between shared state and per-request state.
MRO is the deterministic order in which Python searches for a method or attribute through a class hierarchy. Python 3 uses the C3 Linearization algorithm (also called C3 superclass linearization), which guarantees:
# Diamond inheritance problem
class A:
def method(self) -> str:
return "A"
class B(A):
def method(self) -> str:
return "B"
class C(A):
def method(self) -> str:
return "C"
class D(B, C): # multiple inheritance
pass
d = D()
print(d.method()) # "B" — follows MRO
# View the MRO
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
print([cls.__name__ for cls in D.__mro__])
# ['D', 'B', 'C', 'A', 'object']
# super() follows MRO — not just the immediate parent
class Base:
def greet(self) -> str:
return "Hello from Base"
class Left(Base):
def greet(self) -> str:
return f"Left -> {super().greet()}"
class Right(Base):
def greet(self) -> str:
return f"Right -> {super().greet()}"
class Child(Left, Right):
def greet(self) -> str:
return f"Child -> {super().greet()}"
print(Child().greet())
# Child -> Left -> Right -> Hello from Base
# super() in Left calls Right (not Base directly) — cooperative MRO
Use Case: Django's class-based views (e.g., CreateView(LoginRequiredMixin, CreateView)) rely on C3 MRO to ensure mixins are applied in the correct order — LoginRequiredMixin.dispatch() runs before CreateView.dispatch(), enforcing authentication.
Multiple inheritance allows a class to inherit from more than one parent class, acquiring all their attributes and methods. Python resolves method lookup order using the C3 MRO algorithm, and super() cooperatively chains calls through the full MRO.
Use it when:
class View(LoginRequiredMixin, PermissionMixin, BaseView)Avoid it when:
# Mixin pattern — the idiomatic use of multiple inheritance
class LogMixin:
def log(self, msg: str) -> None:
print(f"[{self.__class__.__name__}] {msg}")
class SerializeMixin:
def to_dict(self) -> dict:
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
class TimestampMixin:
from datetime import datetime
created_at: str = ""
def set_timestamp(self) -> None:
from datetime import datetime
self.created_at = datetime.utcnow().isoformat()
# Combine mixins into a concrete class
class Order(LogMixin, SerializeMixin, TimestampMixin):
def __init__(self, order_id: str, amount: float) -> None:
self.order_id = order_id
self.amount = amount
self.set_timestamp()
def process(self) -> None:
self.log(f"Processing order {self.order_id}")
o = Order("ORD-001", 99.99)
o.process()
print(o.to_dict())
# {'order_id': 'ORD-001', 'amount': 99.99, 'created_at': '2025-...'}
# MRO shows resolution order
print([cls.__name__ for cls in Order.__mro__])
# ['Order', 'LogMixin', 'SerializeMixin', 'TimestampMixin', 'object']
Use Case: Django's class-based views compose LoginRequiredMixin, PermissionRequiredMixin, and generic.ListView via multiple inheritance — each mixin adds one cross-cutting concern (auth, permissions, rendering) without modifying the base class.
A metaclass is the class of a class — it controls how classes themselves are created. When Python executes a class statement, it calls the metaclass to construct the class object. type is the default metaclass for all Python classes.
The metaclass receives: (name, bases, namespace) and returns a new class object. Custom metaclasses allow you to intercept and modify class creation.
# type IS the metaclass of all classes
print(type(int)) # <class 'type'>
print(type(str)) # <class 'type'>
print(type(type)) # <class 'type'> — type is its own metaclass
# Custom metaclass
class SingletonMeta(type):
"""Metaclass that enforces the Singleton pattern on any class."""
_instances: dict = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class DatabasePool(metaclass=SingletonMeta):
def __init__(self, size: int = 5) -> None:
self.size = size
print(f"Pool created with size {size}")
pool1 = DatabasePool(5) # "Pool created with size 5"
pool2 = DatabasePool(10) # silent — returns existing instance
print(pool1 is pool2) # True
# Metaclass that auto-registers subclasses
class PluginMeta(type):
registry: dict[str, type] = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if bases: # skip the base class itself
PluginMeta.registry[name] = cls
return cls
class Plugin(metaclass=PluginMeta):
pass
class CSVPlugin(Plugin): pass
class JSONPlugin(Plugin): pass
print(PluginMeta.registry)
# {'CSVPlugin': <class 'CSVPlugin'>, 'JSONPlugin': <class 'JSONPlugin'>}
When to use: Metaclasses are rarely needed. Prefer class decorators or
__init_subclass__for most cases. Use metaclasses only when you need to intercept class creation itself (ORMs, plugin systems, API schema generation).
Use Case: SQLAlchemy's DeclarativeMeta metaclass scans class attributes at definition time to build the ORM mapping, table schema, and column metadata — enabling class User(Base): id = Column(Integer) syntax without any explicit registration call.
A property is a descriptor that turns method calls into attribute access syntax. It lets you add validation, computed values, or lazy loading without breaking the public API — callers use obj.x regardless of whether x is a plain attribute or a property.
@property adds a getter; @x.setter / @x.deleter add write/delete access.
class Temperature:
def __init__(self, celsius: float) -> None:
self._celsius = celsius # private backing attribute
@property
def celsius(self) -> float:
"""Getter — called on `obj.celsius`"""
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
"""Setter — called on `obj.celsius = value`"""
if value < -273.15:
raise ValueError(f"Temperature {value}°C below absolute zero")
self._celsius = value
@celsius.deleter
def celsius(self) -> None:
del self._celsius
@property
def fahrenheit(self) -> float:
"""Computed / read-only property — no setter"""
return self._celsius * 9 / 5 + 32
@property
def kelvin(self) -> float:
return self._celsius + 273.15
t = Temperature(100)
print(t.celsius) # 100 (getter)
print(t.fahrenheit) # 212.0 (computed, no backing attribute)
t.celsius = 0 # setter with validation
print(t.kelvin) # 273.15
try:
t.celsius = -300 # raises ValueError
except ValueError as e:
print(e)
# t.fahrenheit = 500 # AttributeError: can't set attribute (no setter)
Why not just make it a plain attribute?
obj.x → obj.x())obj.x syntax while adding logic invisiblyUse Case: A REST API model class uses @property for full_name = f"{first} {last}" and a password setter that automatically hashes input with bcrypt — the API surface remains clean attribute access while security logic is enforced transparently.
Polymorphism means “many forms” — the same interface (method name / operator) behaves differently depending on the object's type. Python supports two kinds:
from abc import ABC, abstractmethod
from typing import Protocol
# 1. Subtype polymorphism — classic OOP
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
class Circle(Shape):
def __init__(self, radius: float) -> None:
self.radius = radius
def area(self) -> float:
import math
return math.pi * self.radius ** 2
def perimeter(self) -> float:
import math
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, w: float, h: float) -> None:
self.w, self.h = w, h
def area(self) -> float:
return self.w * self.h
def perimeter(self) -> float:
return 2 * (self.w + self.h)
def print_shape_info(shape: Shape) -> None:
print(f"{type(shape).__name__}: area={shape.area():.2f}, "
f"perimeter={shape.perimeter():.2f}")
shapes: list[Shape] = [Circle(5), Rectangle(4, 6), Circle(2)]
for s in shapes:
print_shape_info(s) # same call, different behaviour
# 2. Duck typing polymorphism — no inheritance needed
class Renderer:
def render(self, template: str) -> str:
return f"<html>{template}</html>"
class EmailRenderer:
def render(self, template: str) -> str:
return f"Subject: {template}"
def send_notification(renderer, msg: str) -> None:
print(renderer.render(msg)) # works for any object with .render()
send_notification(Renderer(), "Hello")
send_notification(EmailRenderer(), "Hello") # no common base class needed
When to use:
Use Case: A report generation service stores a list of ReportSection objects (charts, tables, text blocks). Calling section.render(canvas) on each produces correct output without the caller knowing the concrete type — adding a new MapSection requires zero changes to the rendering loop.
self?A class is a blueprint for creating objects, bundling data (attributes) and behaviour (methods) together. An instance is a concrete object created from a class.
self is a reference to the current instance — the first parameter of every instance method. Python passes it automatically when you call instance.method(); you name it self by convention (not a keyword).
class BankAccount:
# Class variable — shared by all instances
bank_name: str = "PyBank"
_total_accounts: int = 0
def __init__(self, owner: str, balance: float = 0.0) -> None:
# self refers to the new instance being constructed
self.owner = owner # instance variable
self._balance = balance # private instance variable
BankAccount._total_accounts += 1
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self._balance += amount # self refers to THIS account
def withdraw(self, amount: float) -> None:
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
def get_balance(self) -> float:
return self._balance
def __repr__(self) -> str:
return f"BankAccount(owner={self.owner!r}, balance={self._balance:.2f})"
@classmethod
def total_accounts(cls) -> int: # cls = the class itself
return cls._total_accounts
@staticmethod
def validate_amount(amount: float) -> bool:
return amount > 0
# Creating instances — __init__ called automatically
alice = BankAccount("Alice", 1000.0)
bob = BankAccount("Bob") # balance defaults to 0.0
# self is passed automatically — alice is 'self' in deposit()
alice.deposit(500)
print(alice.get_balance()) # 1500.0
# Both instances are independent
bob.deposit(200)
print(bob.get_balance()) # 200.0
print(BankAccount.total_accounts()) # 2
print(repr(alice)) # BankAccount(owner='Alice', balance=1500.00)
Why self is explicit: Unlike Java/C++ this, Python's explicit self makes it immediately clear that a method accesses instance state — improving readability and enabling the method-as-function duality (BankAccount.deposit(alice, 500) is valid).
Use Case: An e-commerce order system models Order as a class — each instance holds its own items, status, and customer_id, while shared business rules (tax rate, currency) live as class variables or static methods.
isinstance()isinstance(object, classinfo) returns True if object is an instance of classinfo or any of its subclasses. It is type-hierarchy aware — unlike type(x) == SomeClass which only matches the exact type.
# Basic usage
print(isinstance(42, int)) # True
print(isinstance(42, float)) # False
print(isinstance(True, int)) # True — bool IS a subclass of int
print(isinstance("hi", (int, str))) # True — second arg can be a tuple of types
# Hierarchy awareness
class Animal: pass
class Dog(Animal): pass
class GoldenRetriever(Dog): pass
gr = GoldenRetriever()
print(isinstance(gr, GoldenRetriever)) # True
print(isinstance(gr, Dog)) # True — subclass check
print(isinstance(gr, Animal)) # True — transitive
print(isinstance(gr, str)) # False
# type() vs isinstance() — key difference
print(type(True) == int) # False — exact type match only
print(isinstance(True, int)) # True — hierarchy-aware
# Using with Abstract Base Classes
from collections.abc import Mapping, Sequence, Iterable
print(isinstance({}, Mapping)) # True
print(isinstance([], Sequence)) # True
print(isinstance("abc", Sequence)) # True
print(isinstance(range(5), Iterable)) # True
# Practical: safe attribute access
def process(value: object) -> str:
if isinstance(value, str):
return value.upper()
elif isinstance(value, (int, float)):
return f"{value:.2f}"
elif isinstance(value, list):
return ", ".join(str(v) for v in value)
else:
return repr(value)
print(process("hello")) # HELLO
print(process(3.14)) # 3.14
print(process([1, 2, 3])) # 1, 2, 3
Use Case: A serialization library uses isinstance(value, Mapping) rather than isinstance(value, dict) so it correctly handles OrderedDict, defaultdict, and custom Mapping subclasses — making the serializer work with any dict-like object.
| Method type | First param | Access | Decorator | Use when |
|---|---|---|---|---|
| Instance method | self (instance) |
Instance + class state | (none) | Needs access to instance data |
| Class method | cls (class) |
Class state only | @classmethod |
Alternative constructors, factory methods |
| Static method | (none) | Neither | @staticmethod |
Utility functions logically grouped with the class |
from datetime import datetime
class User:
_count: int = 0
def __init__(self, name: str, email: str) -> None:
self.name = name
self.email = email
self.created_at = datetime.utcnow()
User._count += 1
# Instance method — has access to self (instance attributes)
def greet(self) -> str:
return f"Hi, I'm {self.name} <{self.email}>"
def is_active(self) -> bool:
age_days = (datetime.utcnow() - self.created_at).days
return age_days < 365
# Class method — receives class, not instance
@classmethod
def from_dict(cls, data: dict) -> "User":
"""Alternative constructor — factory pattern."""
return cls(name=data["name"], email=data["email"])
@classmethod
def count(cls) -> int:
return cls._count
# Static method — no self or cls; logically belongs here
@staticmethod
def validate_email(email: str) -> bool:
return "@" in email and "." in email.split("@")[-1]
# Usage
u1 = User("Alice", "[email protected]")
u2 = User.from_dict({"name": "Bob", "email": "[email protected]"})
print(u1.greet()) # Hi, I'm Alice <[email protected]>
print(User.count()) # 2
print(User.validate_email("bad")) # False
print(User.validate_email("[email protected]")) # True
# Static methods can also be called on instances (but class is cleaner)
print(u1.validate_email("[email protected]")) # True
Use Case: An ORM Model class uses @classmethod from_row(cls, row) as a factory to construct model instances from database cursor rows — enabling User.from_row(row) without exposing the internal column mapping to callers.
This distinction only matters for Python 2. In Python 3 all classes are new-style.
| Feature | Old-style (Python 2 only) | New-style (Python 2.2+ / Python 3 all) |
|---|---|---|
| Declaration | class Foo: |
class Foo(object): or just class Foo: in Py3 |
| Base class | No implicit base | Implicitly inherits from object |
| MRO algorithm | Depth-first (DFS), left to right | C3 Linearization |
super() |
Broken in diamond inheritance | Works correctly |
type(instance) |
<type 'instance'> |
<class '__main__.Foo'> |
Descriptors, __slots__, properties |
Not fully supported | Fully supported |
# Python 3 — ALL classes are new-style automatically
class Animal: # implicitly: class Animal(object):
def speak(self) -> str:
return "..."
class Dog(Animal):
def speak(self) -> str:
return "Woof"
class Cat(Animal):
def speak(self) -> str:
return "Meow"
class PoliceDog(Dog):
def speak(self) -> str:
return super().speak() + "! (police)" # super() works correctly
# Verify new-style
print(PoliceDog.__bases__) # (<class '__main__.Dog'>,)
print(PoliceDog.__mro__) # correct C3 order
print(isinstance(PoliceDog(), object)) # True — inherits from object
# object gives all classes built-in methods
d = Dog()
print(dir(d)) # __init__, __str__, __repr__, __eq__, __hash__, etc.
# In Python 2 (for reference — do NOT write this)
# class OldStyle: pass # old-style — no object base
# class NewStyle(object): pass # new-style (must be explicit in Py2)
Practical implication today: Always use class Foo: in Python 3 — it's new-style. If maintaining Python 2 legacy code, add (object) explicitly to ensure new-style behaviour.
Use Case: A Django migration from Python 2 to 3 required removing redundant (object) from hundreds of model class definitions — a safe, automated change since Python 3 makes them equivalent.
A metaclass is the class of a class — it controls how class objects are created and can intercept attribute access, method definition, and subclass registration at class creation time. type is the default metaclass for all Python classes.
# type is the metaclass of all classes
print(type(int)) # <class 'type'>
print(type(list)) # <class 'type'>
print(type(type)) # <class 'type'> — type is its own metaclass
# Custom metaclass — intercepts class creation
class ValidatedMeta(type):
"""Ensure all public methods have type annotations."""
def __new__(mcs, name: str, bases: tuple, namespace: dict):
for attr_name, attr_val in namespace.items():
if callable(attr_val) and not attr_name.startswith("_"):
annotations = getattr(attr_val, "__annotations__", {})
if "return" not in annotations:
raise TypeError(
f"{name}.{attr_name}() is missing a return type annotation"
)
return super().__new__(mcs, name, bases, namespace)
class MyService(metaclass=ValidatedMeta):
def greet(self, name: str) -> str: # OK — has return annotation
return f"Hello, {name}"
# def bad_method(self): pass # Would raise TypeError at class definition!
# __init_subclass__ — simpler alternative for subclass hooks (Python 3.6+)
class PluginBase:
_registry: dict[str, type] = {}
def __init_subclass__(cls, plugin_name: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if plugin_name:
PluginBase._registry[plugin_name] = cls
class CSVPlugin(PluginBase, plugin_name="csv"):
def process(self): pass
class JSONPlugin(PluginBase, plugin_name="json"):
def process(self): pass
print(PluginBase._registry)
# {'csv': <class 'CSVPlugin'>, 'json': <class 'JSONPlugin'>}
When to use: Metaclasses are for framework authors. For application code, prefer
__init_subclass__, class decorators, ordataclasses. Use metaclasses only when you need to control how class objects themselves are constructed.
Use Case: SQLAlchemy's DeclarativeMeta scans each model class body at definition time to register column descriptors, build table metadata, and wire up the ORM mapper — all happening before any instance is ever created.
A descriptor is any object that defines __get__, __set__, or __delete__ and is assigned as a class attribute. Python calls these methods automatically when the attribute is accessed on an instance, enabling properties, methods, classmethod, staticmethod, and custom validation attributes.
from typing import Any
# Data descriptor — defines both __get__ and __set__
class TypedAttribute:
"""Descriptor that enforces a type on assignment."""
def __init__(self, name: str, expected_type: type) -> None:
self._name = name
self._type = expected_type
def __set_name__(self, owner: type, name: str) -> None:
self._name = name # called automatically at class creation
def __get__(self, obj: Any, objtype: type = None) -> Any:
if obj is None:
return self # accessed on class, not instance
return obj.__dict__.get(self._name)
def __set__(self, obj: Any, value: Any) -> None:
if not isinstance(value, self._type):
raise TypeError(
f"{self._name} must be {self._type.__name__}, "
f"got {type(value).__name__}"
)
obj.__dict__[self._name] = value
class Product:
name = TypedAttribute("name", str)
price = TypedAttribute("price", float)
stock = TypedAttribute("stock", int)
def __init__(self, name: str, price: float, stock: int) -> None:
self.name = name # triggers TypedAttribute.__set__
self.price = price
self.stock = stock
p = Product("Widget", 9.99, 100)
print(p.name) # Widget
try:
p.price = "expensive" # raises TypeError
except TypeError as e:
print(e) # price must be float, got str
# Non-data descriptor — only __get__ (e.g., functions / methods)
class LazyProperty:
"""Compute once, cache on instance."""
def __init__(self, func):
self._func = func
self.__doc__ = func.__doc__
def __set_name__(self, owner, name):
self._attr = name
def __get__(self, obj, objtype=None):
if obj is None: return self
value = self._func(obj)
obj.__dict__[self._attr] = value # cache — shadows descriptor
return value
class Circle:
def __init__(self, radius: float) -> None:
self.radius = radius
@LazyProperty
def area(self) -> float:
import math
print("computing area...")
return math.pi * self.radius ** 2
c = Circle(5)
print(c.area) # "computing area..." then 78.54...
print(c.area) # no print — cached in instance __dict__
Use Case: Django's ORM Field objects (e.g., CharField, IntegerField) are descriptors — model.field_name triggers __get__ which queries the deferred value, and assignment triggers __set__ which validates and stores the value before save().
MRO is the deterministic order in which Python searches a class hierarchy to resolve a method or attribute lookup. Python 3 uses the C3 Linearization algorithm, which guarantees:
# Diamond inheritance — the classic MRO challenge
class A:
def method(self) -> str: return "A"
class B(A):
def method(self) -> str: return "B"
class C(A):
def method(self) -> str: return "C"
class D(B, C): # diamond: D → B → C → A → object
pass
print(D().method()) # "B" — first in MRO
# Inspect MRO
print([cls.__name__ for cls in D.__mro__])
# ['D', 'B', 'C', 'A', 'object']
# super() cooperatively follows MRO
class Base:
def process(self) -> list:
return ["Base"]
class Left(Base):
def process(self) -> list:
return ["Left"] + super().process() # calls Right, not Base directly
class Right(Base):
def process(self) -> list:
return ["Right"] + super().process()
class Child(Left, Right):
def process(self) -> list:
return ["Child"] + super().process()
print(Child().process())
# ['Child', 'Left', 'Right', 'Base']
# MRO: Child → Left → Right → Base → object
# Mixin pattern relying on MRO
class LogMixin:
def save(self) -> None:
print(f"[LOG] Saving {self.__class__.__name__}")
super().save() # calls next in MRO
class Model:
def save(self) -> None:
print("Model.save()")
class User(LogMixin, Model): # LogMixin.save() → Model.save()
pass
User().save()
# [LOG] Saving User
# Model.save()
Use Case: Django's class-based views depend on MRO so that LoginRequiredMixin (placed first) has its dispatch() called before the view's dispatch(), intercepting unauthenticated requests without modifying DetailView at all.
from __future__ import annotations
from dataclasses import dataclass, field
from typing import ClassVar
# 1. Basic class with __init__
class Rectangle:
# Class variable — shared by all instances
shape_type: ClassVar[str] = "rectangle"
def __init__(self, width: float, height: float) -> None:
self.width = width # instance variable
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
def __repr__(self) -> str:
return f"Rectangle(width={self.width}, height={self.height})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, Rectangle): return NotImplemented
return self.width == other.width and self.height == other.height
r = Rectangle(4, 6)
print(r.area()) # 24.0
print(repr(r)) # Rectangle(width=4, height=6)
# 2. Inheritance
class Square(Rectangle):
def __init__(self, side: float) -> None:
super().__init__(side, side) # delegate to parent
def __repr__(self) -> str:
return f"Square(side={self.width})"
s = Square(5)
print(s.area()) # 25.0
print(isinstance(s, Rectangle)) # True
# 3. dataclass — auto-generates __init__, __repr__, __eq__
@dataclass
class Point:
x: float
y: float
label: str = "origin"
_history: list = field(default_factory=list, repr=False)
def distance_to(self, other: Point) -> float:
return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5
p1 = Point(0, 0)
p2 = Point(3, 4)
print(p1.distance_to(p2)) # 5.0
print(p2) # Point(x=3, y=4, label='origin')
Use Case: A domain model layer defines @dataclass classes (Order, LineItem, Customer) with type annotations — getting __repr__, __eq__, and __init__ for free while keeping the model readable and mypy-checked.
Instantiation calls the metaclass (type.__call__) which first calls __new__ (allocate memory, return the new object) then __init__ (initialize the object). The result is the fully constructed instance.
class Connection:
def __new__(cls, *args, **kwargs):
print(f"__new__: allocating {cls.__name__}")
instance = super().__new__(cls) # allocate memory
return instance
def __init__(self, host: str, port: int) -> None:
print(f"__init__: initializing with {host}:{port}")
self.host = host
self.port = port
def __repr__(self) -> str:
return f"Connection({self.host!r}, {self.port})"
# Instantiation — calls __new__ then __init__
conn = Connection("localhost", 5432)
# __new__: allocating Connection
# __init__: initializing with localhost:5432
print(conn) # Connection('localhost', 5432)
# Multiple instances are independent objects
conn2 = Connection("db.prod", 5432)
print(conn is conn2) # False — different objects
print(id(conn), id(conn2))
# Singleton via __new__
class Config:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self) -> None:
if not hasattr(self, "_initialized"):
self.debug = False
self._initialized = True
cfg1 = Config()
cfg2 = Config()
print(cfg1 is cfg2) # True — same object
# Named constructors (alternative instantiation patterns)
from datetime import date
d1 = date(2025, 6, 15) # standard __init__
d2 = date.today() # classmethod factory
d3 = date.fromisoformat("2025-06-15") # another classmethod
print(type(d1) is type(d2) is type(d3)) # True — all date instances
Use Case: An ORM Session factory uses __new__ to return an existing session from the pool instead of creating a new object, making Session() syntax act as a transparent pool checkout without callers knowing about pooling.
Try, except and finally blocks are used in Python error handling. Code is executed in the try block until an error occurs. One can use a generic except block, which will receive control after all errors, or one can use specific exception handling blocks for various error types. Control is transferred to the appropriate except block. In all cases, the finally block is executed. Raise may be used to raise your own exceptions.
Code Example:
try:
….#This can be any code
except:
…# error handling code goes here
finally:
…# code that will be executed regardless of exception handling goes here.
The errors which occurs after starting the execution of the programs are known as runtime errors. Runtime errors can occur because of:
Invalid Input
Invalid Logic
Memory issues
Hardware failures and so on
With respect to every reason which causes to runtime error correspoing runtime error representation class is available Runtime error representation classes technically we call as a exception classes. While executing the program if any runtime error is occur corresponding runtime error representation class object is created Creating runtime error representation class object is technically known as a rising exception While executing the program if any exception is raised, then internally python interpreter verify any code is implemented to handle raised exception or not If code is not implemented to handle raised exception then program will be terminated abnormally
The concept of terminating the program in the middle of its execution without executing last statement of the main module is known as a abnormal termination Abnormal termination is undesirable situation in programming languages.
try Block?A block which is preceded by the try keyword is known as a try block Syntax: try{ //statements that may cause an exception }
The statements which causes to run time errors and other statements which depends on the execution of run time errors statements are recommended to represent in try block While executing try block statement if any exception is raised then immediately try block identifies that exception, receive that exception and forward that exception to except block without executing remaining statements to try block.
__init__ statement?If __init__ raises an exception, the object construction fails. __new__ has already allocated memory for the object, but since __init__ did not complete, the partially-constructed object is not returned to the caller — the exception propagates instead. Python's garbage collector will eventually reclaim the allocated memory via reference counting.
class DatabaseConnection:
_instances: list["DatabaseConnection"] = []
def __init__(self, host: str, port: int) -> None:
if not (1 <= port <= 65535):
raise ValueError(f"Invalid port: {port}")
if not host:
raise ValueError("Host cannot be empty")
# If an error occurs here, __init__ partially ran
self.host = host
self.port = port
self._connection = self._connect() # might raise ConnectionError
DatabaseConnection._instances.append(self) # only reached if no error
def _connect(self):
# Simulate connection failure
raise ConnectionError(f"Cannot reach {self.host}:{self.port}")
# Test: error in validation
try:
conn = DatabaseConnection("localhost", 99999)
except ValueError as e:
print(f"ValueError: {e}") # Invalid port: 99999
# conn is never bound — the object does not exist
# Test: error mid-init (after self.host assigned but before append)
try:
conn2 = DatabaseConnection("db.prod", 5432)
except ConnectionError as e:
print(f"ConnectionError: {e}")
# DatabaseConnection._instances is still empty — append never ran
print(DatabaseConnection._instances) # []
Key takeaway: Always use try/except or contextlib.ExitStack in __init__ to clean up resources (file handles, sockets) if later steps fail — Python does not call __del__ on the partially-constructed object reliably.
Use Case: A connection pool factory wraps __init__ errors to ensure that failed connection attempts are logged and retried without leaving zombie objects registered in the pool registry.
Integer or float division by zero raises ZeroDivisionError. The // (floor division) and % (modulo) operators also raise it. The only exception: float('inf') / 0 does not raise — it follows IEEE 754 and returns inf.
# Integer division by zero
try:
result = 10 // 0
except ZeroDivisionError as e:
print(f"ZeroDivisionError: {e}") # integer division or modulo by zero
# Float division by zero
try:
result = 10.0 / 0.0
except ZeroDivisionError as e:
print(f"ZeroDivisionError: {e}") # float division by zero
# Modulo by zero
try:
result = 7 % 0
except ZeroDivisionError as e:
print(f"ZeroDivisionError: {e}") # integer division or modulo by zero
# Safe division utility
def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
"""Return numerator/denominator, or default if denominator is zero."""
if denominator == 0:
return default
return numerator / denominator
print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # 0.0
# In numpy, division by zero produces inf/nan (not an exception)
import numpy as np
arr = np.array([1.0, 2.0, 0.0])
with np.errstate(divide="ignore", invalid="ignore"):
result = np.array([1.0, 2.0]) / arr[:2]
Use Case: A financial analytics system wraps all division operations in safe_divide() to handle missing or zero-denominator data gracefully, returning None instead of crashing — critical for ratio metrics like conversion rate where denominators can be zero.
Python uses try / except / else / finally blocks. The else clause runs only if no exception was raised; finally always runs (cleanup).
[Page truncated to keep GeezerVison snappy.]