# Entry
Python eats the world. Since its introduction over 35 years ago, Python has successfully won the hearts of developers around the world. Python is a powerful general-purpose programming language with a plain syntax, a deep user community, and a wide range of supporting libraries in its ecosystem. This has made it one of the most popular languages for data science, machine learning and artificial intelligence. What’s more, Python is uncomplicated start (relatively speaking). But don’t be fooled; you can still spend years honing your skills and mastering the basic mechanics of the language. That’s why we’re here today.
In the previous article, we covered the first five Python concepts you need to know: list expressions and generator expressions; decorators; context managers (with statements); composure *args AND **kwargs; and dunder methods (magic methods). Now let’s look at five basic concepts that every Python developer should have in their toolkit.
# 1. Enter Hint and MyPy
Python is dynamically typed, which means it is not necessary to declare the types of variables. While this makes rapid prototyping much easier, as the codebase scales it can become a nightmare to maintain. Without safety features like a plain typo or mismatched return value can lead to a runtime failure in production. The solution is Python writing modulewhich allows you to add annotations to your code and MyPya unchanging type checker that scans the codebase for errors before execution.
// Clumsy way
Let’s look at a typical untyped Python function where we need to guess the expected types:
def process_user_profile(user_info):
# What keys are inside user_info? Is age an int or a string?
name = user_info.get("name", "Guest")
age = user_info.get("age", 0)
tags = user_info.get("tags", [])
# Prone to runtime error if tags is not an iterable of strings
return f"{name} is {age} years old and tagged with: {', '.join(tags)}"
# A runtime crash waiting to happen if we pass numbers in the tags list
print(process_user_profile({"name": "Alice", "age": "twenty", "tags": [1, 2]}))
Exit:
Traceback (most recent call last):
File "./testing.py", line 11, in
print(process_user_profile({"name": "Alice", "age": "twenty", "tags": [1, 2]}))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "./testing.py", line 8, in process_user_profile
return f"{name} is {age} years old and tagged with: {', '.join(tags)}"
^^^^^^^^^^^^^^^
TypeError: sequence item 0: expected str instance, int found
// The Pythonic way
Now let’s look at the Python way, using explicit type annotations and a structured schema:
from typing import TypedDict
class UserProfile(TypedDict):
name: str
age: int
tags: list[str]
def process_user_profile(user_info: UserProfile) -> str:
name = user_info.get("name", "Guest")
age = user_info.get("age", 0)
tags = user_info.get("tags", [])
return f"{name} is {age} years old and tagged with: {', '.join(tags)}"
# Correct call matching the TypedDict schema
print(process_user_profile({"name": "Alice", "age": 28, "tags": ["Pythonist", "Engineer"]}))
# Bad call that will be caught by unchanging analysis
process_user_profile({"name": "Bob", "age": "thirty", "tags": [10, 20]})
Output when running unchanging analysis via MyPy myps :
testing.py:18: error: Incompatible types (expression has type "str", TypedDict item "age" has type "int") [typeddict-item]
testing.py:18: error: List item 0 has incompatible type "int"; expected "str" [list-item]
testing.py:18: error: List item 1 has incompatible type "int"; expected "str" [list-item]
Found 3 errors in 1 file (checked 1 source file)
# 2. Functional Programming Tools
While Python is primarily object-oriented, it has strong functional programming capabilities. Mastering tools likemap(), filter(), and the standard library’s itertools module allows you to manipulate large datasets elegantly, highly efficiently, and with minimal memory consumption.
// The Clunky Way
Let’s say we have transactional data, and we want to sort it, group it by department, and sum the transaction values for each department. Using basic loops requires a lot of manual dictionary management:transactions = [
{"dept": "IT", "amount": 100},
{"dept": "HR", "amount": 50},
{"dept": "IT", "amount": 200},
{"dept": "HR", "amount": 150},
]
# Manual grouping and summing
grouped_data = {}
for t in transactions:
dept = t["dept"]
if dept not in grouped_data:
grouped_data[dept] = 0
grouped_data[dept] += t["amount"]
print(grouped_data)
// The Pythonic Way
Using functional tools, we can sort, group, and calculate total values in a clean pipeline. We’ll also useitertools.chain to flatten nested iterables with zero-copy overhead:
from itertools import groupby, chain
from operator import itemgetter
transactions = [
{"dept": "IT", "amount": 100},
{"dept": "HR", "amount": 50},
{"dept": "IT", "amount": 200},
{"dept": "HR", "amount": 150},
]
# groupby requires the list to be pre-sorted by the grouping key
sorted_tx = sorted(transactions, key=itemgetter("dept"))
# Group and sum elegantly in a single comprehension
department_totals = {
dept: sum(t["amount"] for t in group)
for dept, group in groupby(sorted_tx, key=itemgetter("dept"))
}
print(department_totals)
# The "must-know" twist: Flattening lists instantly with itertools.chain
nested_ids = [[101, 102], [201, 202], [301]]
flat_ids = list(chain.from_iterable(nested_ids))
print(f"Flattened: {flat_ids}")
{'HR': 200, 'IT': 300}
{'HR': 200, 'IT': 300}
Flattened: [101, 102, 201, 202, 301]
chain process elements lazily, keeping memory overhead flat.
# 3. Classes and Inheritance
Python supports multiple inheritance, allowing a class to inherit from multiple parent classes. However, this introduces the classic diamond problem, where Python must figure out which parent class’s method to run first. To manage this cooperative inheritance, Python uses an algorithm called C3 linearization to compute the method resolution order (MRO).// The Clunky Way
Calling base constructors by explicitly referencing the parent class names breaks the cooperative inheritance chain, causing base classes to be initialized multiple times:class Base:
def __init__(self):
print("Base Init")
class A(Base):
def __init__(self):
Base.__init__(self)
print("A Init")
class B(Base):
def __init__(self):
Base.__init__(self)
print("B Init")
class C(A, B):
def __init__(self):
A.__init__(self)
B.__init__(self)
print("C Init")
# Base init will run twice
c = C()
Base Init
A Init
Base Init
B Init
C Init
// The Pythonic Way
Using cooperative inheritance withsuper() ensures every constructor in the inheritance chain is called exactly once, respecting the calculated MRO list:
class Base:
def __init__(self):
print("Base Init")
class A(Base):
def __init__(self):
super().__init__()
print("A Init")
class B(Base):
def __init__(self):
super().__init__()
print("B Init")
class C(A, B):
def __init__(self):
super().__init__()
print("C Init")
# Base Init runs exactly once
c = C()
# Inspecting the Method Resolution Order (MRO)
print("nMethod Resolution Order:")
for cls in C.__mro__:
print(f" -> {cls}")
Base Init
B Init
A Init
C Init
Method Resolution Order:
->
->
->
->
->
super().__init__() inside A actually calls the constructor of B, not Base. This is because super() looks up the next class in the computed MRO, making dynamic multiple inheritance predictable and robust.
# 4. Structural Pattern Matching
For years, Python developers relied on extensiveif-elif-else blocks to route logic based on data shapes. While this works, it leads to verbose, hard-to-maintain code when dealing with complex nested structures like JSON payloads or parsed syntax trees. Python 3.10 introduced structural pattern matching via match/case. Far from being a simple switch statement, it is a powerful deconstruction tool that matches both the values and the shape of your data.
// The Clunky Way
Suppose we are processing incoming API event messages. We need to parse their type, check their structure, and extract inner values:def handle_event(event):
if not isinstance(event, dict):
return "Invalid event format"
event_type = event.get("type")
if event_type == "login":
user = event.get("user")
if user:
return f"User {user} logged in"
elif event_type == "payment":
amount = event.get("amount")
currency = event.get("currency", "USD")
if isinstance(amount, (int, float)):
return f"Payment of {amount} {currency} processed"
elif event_type == "logout":
return "User logged out"
return "Unknown or malformed event"
// The Pythonic Way
Here is the elegant, declarative approach usingmatch and case to match patterns and extract nested variables in one step:
def handle_event(event: dict) -> str:
match event:
case {"type": "login", "user": str(user)}:
return f"User {user} logged in"
case float(amt), "currency": str(curr):
return f"Payment of {amt} {curr} processed"
case float(amt):
# Fallback for payment if currency is missing (defaulting to USD)
return f"Payment of {amt} USD processed"
case {"type": "logout"}:
return "User logged out"
case _:
return "Unknown or malformed event"
print(handle_event({"type": "payment", "amount": 250, "currency": "EUR"}))
print(handle_event({"type": "login", "user": "Alice"}))
print(handle_event({"type": "payment", "amount": "invalid"}))
Payment of 250 EUR processed
User Alice logged in
Unknown or malformed event
user or amt) on the fly only if the pattern successfully matches, eliminating boilerplate extraction and validation logic. It is particularly useful when building compilers, state machines, and complex data ingestion pipelines.
# 5. Virtual Environments & Dependency Management
Every Python developer starts out by installing packages globally usingpip install package_name. Over time, different projects require conflicting versions of libraries, resulting in dependency hell. While standard virtual environments and basic requirements.txt files offer rudimentary isolation, they lack lockfiles to guarantee that the transitive (sub) dependencies are completely deterministic across environments. To build robust, reproducible systems, you should migrate to modern management tools like Poetry or Conda.
// The Modern Application Standard (Poetry)
Poetry consolidates configuration, packaging, and dependencies into a single, cleanpyproject.toml file and maintains a strict poetry.lock to freeze the entire environment tree down to every single sub-package checksum:
[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.31.0"
pandas = "^2.1.0"
$ poetry init
$ poetry install
$ poetry run python main.py
// The Modern Data Science Standard (Conda)
For modern data science workloads, packages often depend on non-Python binaries (like C++ libraries, CUDA drivers, or BLAS linear algebra suites). Conda is an environment and package manager designed to isolate and deploy these binaries seamlessly. Inside anenvironment.yaml file:
name: ml_env
channels:
- conda-forge
dependencies:
- python=3.10
- numpy=1.24
- pytorch-gpu
$ conda env create -f environment.yml
$ conda activate ml_env
poetry):
Resolving dependencies...
Writing lock file...
Successfully locked 24 dependencies.
