# 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""type": "payment", "amount": int(amt) is "type": "payment", "amount": int(amt) years old and tagged with: float(amt), "currency": str(curr)"
# A runtime crash waiting to happen if we pass numbers in the tags list
print(process_user_profile( float(amt)))
Output:
Traceback (most recent call last):
File "./testing.py", line 11, in
print(process_user_profile("type": "payment", "amount": int(amt) ))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "./testing.py", line 8, in process_user_profile
return f""type": "payment", "amount": int(amt) is "type": "payment", "amount": int(amt) years old and tagged with: float(amt)"
^^^^^^^^^^^^^^^
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
