5 More Must-Know Python Concepts
Let's take a look at five more fundamental concepts that every Python developer should have in their toolkit.

# Introduction
Python is eating the world. Since its introduction over 35 years ago, Python has successfully bullied its way into the hearts of programmers the world over. Python is a powerful, general-purpose programming language with a simple syntax, deep user community, and a vast array of supporting libraries in its ecosystem. This has helped make it one of the go-to languages of data science, machine learning and AI. Moreover, Python is easy to get started with (relatively speaking). Don't be fooled, however; you can still spend years improving your skills and mastering the core mechanisms of the language. That's why we're here today.
In a previous article, we covered our first five must-know Python concepts: list comprehensions and generator expressions; decorators; context managers (with statements); mastering *args and **kwargs; and dunder methods (magic methods). Now, let's take a look at five more fundamental concepts that every Python developer should have in their toolkit.
# 1. Type Hinting & MyPy
Python is dynamically typed, meaning that it isn't necessary to declare variable types. While this makes rapid prototyping much easier, it can become a maintenance nightmare as your codebase scales. Without type safety, a simple typo or mismatched return value can lead to runtime crashes in production. The solution is Python's typing module, which allows you to annotate your code, and MyPy, a static type checker that scans your codebase for errors before execution.
// The Clunky Way
Let's look at a typical, untyped Python function where we must 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]}))
Output:
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 take a look at the Pythonic 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 static analysis
process_user_profile({"name": "Bob", "age": "thirty", "tags": [10, 20]})
Output when running MyPy static analysis via mypy <script_name.py>:
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)
Using type annotations makes your code self-documenting, allowing IDEs to provide flawless autocompletion and highlight bugs instantly. Integrating MyPy into your CI/CD pipeline ensures type mismatches are blocked before your code reaches anywhere close to production.
# 2. Functional Programming Tools
While Python is primarily object-oriented, it has strong functional programming capabilities. Mastering tools like map(), 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 use itertools.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}")
Output:
{'HR': 200, 'IT': 300}
{'HR': 200, 'IT': 300}
Flattened: [101, 102, 201, 202, 301]
Functional pipelines are not just cleaner, they are also often faster because iteration is pushed to highly optimized C-level internals. Additionally, tools like 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()
Output:
Base Init
A Init
Base Init
B Init
C Init
// The Pythonic Way
Using cooperative inheritance with super() 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}")
Output:
Base Init
B Init
A Init
C Init
Method Resolution Order:
-> <class '__main__.C'>
-> <class '__main__.A'>
-> <class '__main__.B'>
-> <class '__main__.Base'>
-> <class 'object'>
Notice that in cooperative inheritance, 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 extensive if-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 using match 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 {"type": "payment", "amount": int(amt) | float(amt), "currency": str(curr)}:
return f"Payment of {amt} {curr} processed"
case {"type": "payment", "amount": int(amt) | 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"}))
Output:
Payment of 250 EUR processed
User Alice logged in
Unknown or malformed event
Structural pattern matching binds variables (like 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 using pip 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, clean pyproject.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"
And here are the commands to create, lock, and run environments:
$ 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 an environment.yaml file:
name: ml_env
channels:
- conda-forge
dependencies:
- python=3.10
- numpy=1.24
- pytorch-gpu
Here are the commands to build and activate a binary-safe environment:
$ conda env create -f environment.yml
$ conda activate ml_env
Output example (when running poetry):
Resolving dependencies...
Writing lock file...
Successfully locked 24 dependencies.
Moving beyond standard pip installs to Poetry or Conda ensures that your application or data science pipeline works exactly the same way on your colleague's machine and the production cloud as it does on your local laptop.
# Wrapping Up
Mastering these five concepts marks the transition from writing scripts to building software. By utilizing type hinting for codebase safety, selecting the correct concurrency models for speed, leveraging functional tools for elegant data flows, respecting the MRO in object-oriented structures, and adopting modern environment systems for reproducibility, you are elevating your Python toolkit to professional engineering standards.
Matthew Mayo (@mattmayo13) holds a master's degree in computer science and a graduate diploma in data mining. As managing editor of KDnuggets & Statology, and contributing editor at Machine Learning Mastery, Matthew aims to make complex data science concepts accessible. His professional interests include natural language processing, language models, machine learning algorithms, and exploring emerging AI. He is driven by a mission to democratize knowledge in the data science community. Matthew has been coding since he was 6 years old.