Level: Intermediate Prerequisites: Basics of Python programming
Table of Contents
- 2.1 Introduction to PEP 8
- 2.2 PEP 8 Rules Overview
- 2.3 Demo: PEP 8 in practice
- 2.4 Summary: PEP 8
- 2.5 Demo: Check your code with Pylint
- 2.6 Demo: Check your code with Flake8
- 2.7 Demo: Correct your formatting with Black
- 2.8 Demo: Use verification tools from VS Code
- 2.9 Summary: Style tools
- 3.1 Introduction: Docstrings and PEP 257
- 3.2 Demo: Generate HTML documentation with Sphinx
- 3.3 Demo: Understanding reStructuredText
- 3.4 Summary: Sphinx and reStructuredText
- 3.5 Demo: Use reStructuredText in docstrings
- 3.6 Demo: Generate code documentation with APIDoc
- 3.7 Summary: Code Documentation
- 4.1 Introduction: Static typing vs. dynamic typing
- 4.2 Demo: Type hints
- 4.3 Understanding type hints
- 4.4 Demo: Apply type hints in a project
- 4.5 Types and classes
- 4.6 Demo: Applying type hints in a project (part 2)
- 4.7 Special types: Union, Any and Collections
- 4.8 Duck Typing
- 4.9 Demo: Mypy
1. Course Overview
This course, Python 3 Best Practices, covers a set of best practices that help improve the quality of Python code. These topics are not about the functional correctness of the code, but rather more indirect aspects of quality:
- PEP 8 — the official Python code formatting guidelines, along with the tools that help comply with them
- Docstrings and Sphinx — how to create beautiful and publishable documentation from Python docstrings
- Type hints — how to improve maintainability and prevent errors by adding type annotations
At the end of this course, you will be able to write clearer, more readable, and more maintainable code, with better documentation.
Prerequisites: Be familiar with the basics of Python programming.
Versions used in this course:
- Python
- Pylint
- Black
- Sphinx
- Mypy
2. Follow Python style conventions: PEP 8
2.1 Introduction to PEP 8
What is a PEP?
The abbreviation PEP stands for Python Enhancement Proposals. Over the years, many of these documents have been published by the Python community. Formally, a PEP is defined as a design document providing information to the Python community, or describing new functionality for Python, its processes, or its environment.
The complete list of PEPs can be found on the official Python website, under Documentation → Developer’s Guide → Quick Links → PEPs. This list references all kinds of information: best practices, guidelines and proposals for new features.
PEP 8: the official style convention
PEP 8 is the central reference document for Python code style. Although there are many PEPs, PEP 8 is the official style guide that dictates how Python code should be written and formatted. The documentation can also be found on the pep8.org site, which offers a nicer formatting of the code examples.
2.2 Overview of PEP 8 rules
When we talk about code style, we are not interested in whether the code works correctly or whether it is efficient. This is a different type of quality: style. Is the code readable? Is it laid out uniformly and coherently? Does it look like other developers’ Python code?
By following the PEP 8 guidelines, we avoid surprising or irritating other Python developers with the placement of parentheses or commas. The code will be easier to read and understand, making maintenance easier.
The key quote from PEP 8
“A foolish consistency is the hobgoblin of little minds.”
Guido van Rossum, the creator of Python, understood very well that code is read much more often than it is written. The ultimate goal of PEP 8 is to improve code readability and make it as consistent as possible. As PEP 8 states:
“A style guide is about consistency. Consistency with a style guide is important, consistency within a project is more important, and consistency within a module or function is most important.”
In other words, PEP 8 must be applied with common sense. If the rule harms readability in a particular case, it is acceptable to ignore it.
The main rules of PEP 8
PEP 8 says how:
- Format Python code and comments
- Using spaces and punctuation in Python
- Name classes, methods, variables, etc.
Here is an overview of the most important rules:
Indentation:
# Correct : 4 espaces par niveau
def ma_fonction():
if condition:
faire_quelque_chose()
Maximum line length:
- Preferably limited to 79 characters
Lines between constructions:
- 2 empty lines between top-level functions and classes
- 1 empty line between methods within the same class
class MaClasse:
def methode_un(self):
pass
def methode_deux(self):
pass
def fonction_niveau_superieur():
pass
class AutreClasse:
pass
Spaces in expressions:
# Correct
x = 1
y = x + 1
liste[1:2]
dict['cle']
# Incorrect
x=1
y = x +1
Organization of imports: Imports from different modules must be on separate lines, and they must be organized into three groups, separated by empty lines:
- Imports from the standard library
- Imports from third-party libraries
- Local imports (project specific)
# Groupe 1 : bibliothèque standard
import os
import random
# Groupe 2 : bibliothèques tierces
import requests
# Groupe 3 : imports locaux
from mypackage import weapons
from mypackage import player
Naming conventions:
| Element | Agreement | Example |
|---|---|---|
| Modules | Short, all lowercase | game, player |
| Classes | CapWords (without underscores) | GamePlayer |
| Functions/methods | Lowercase letters with underscores | get_player_name() |
| Variables | Lowercase letters with underscores | player_health |
| Constants | All caps | MAX_HEALTH = 100 |
| Internal use | Prefixed with an underscore | _internal_var |
Documentation: PEP 8 specifies that we must include a docstring for all public components: modules, functions, classes and methods. There is a separate document, PEP 257, which describes what a docstring should contain.
2.3 Demo: PEP 8 in practice
The demonstration project
To illustrate PEP 8 in practice, a small demonstration project is used throughout the course: a role-playing game in which we simulate a knight fighting against a fire-breathing dragon. The project contains a game.py module with a Game class, as well as player and weapons modules.
PyCharm and PEP 8 warnings
PyCharm helps comply with PEP 8 by displaying warnings in code:
- In the upper right corner, we see the number of warnings and errors of the current file
- Warnings are underlined with small wavy lines in the code
- On mouseover, PyCharm shows details
Examples of common warnings:
PEP 8: E302 expected 2 blank lines, got 1— a blank line is missing before a top-level classPEP 8: W191 indentation contains tabs— using tabs instead of spacesPEP 8: E128 continuation line under-indented for visual indent— bad indentation of a continuation line
Automatic reformatting with PyCharm
PyCharm can automatically reformat the entire code by going to:
- Menu Code → Reformat Code
To reorder the imports:
- Menu Code → Optimize Imports
Continuation lines
When cutting a long line into several lines, PEP 8 gives rules on the indentation of the second line. Example :
# Correct : le premier mot de la seconde ligne s'aligne sur le premier argument
resultat = ma_fonction(argument1,
argument2,
argument3)
# Correct aussi : indentation par rapport au niveau d'ouverture
resultat = ma_fonction(
argument1,
argument2,
argument3,
)
2.4 Summary: PEP 8
PEP 8 gives conventions for code layout:
| Rule | Value |
|---|---|
| Indentation | 4 spaces per level |
| Maximum line length | 79 characters |
| Lines between classes/functions (top) | 2 empty lines |
| Lines between methods (in a class) | 1 empty line |
Organization of imports:
- Standard library
- Third-party libraries (e.g.
requests) - Local imports
Main naming conventions:
- Modules: short lowercase names
- Classes: capitalized names without underscores
- Functions/methods: lowercase names with underscores
- Constants: all uppercase
- Names for internal use: prefixed with an underscore
Documentation: Always include a docstring for public components.
2.5 Demo: Check your code with Pylint
Presentation of Pylint
Pylint is a static code analyzer. It analyzes the code without executing it, looking for:
- Potential errors
- Conformance to code style
- code smells
- Suggestions for refactoring
Pylint is very configurable: you can define the code style for your project, even if it differs from the standard PEP 8 rules.
Pylint works with components called checkers. Among the checkers activated by default:
- Basic checker: checks that the names of variables, functions and classes are correct
- Class checker: checks for class-related issues (access to an attribute before its definition, etc.)
- Design checker: checks design problems (e.g. function with too many arguments)
Installation and use
# Installation dans un virtual environment
pip install pylint
# Vérifier un package entier
pylint gamedemo
# Vérifier un fichier unique
pylint game.py
Examples of Pylint warnings
Pylint can detect issues that PyCharm doesn’t show, like:
- Missing docstrings —
C0114: Missing module docstring - Names too short —
C0103: Attribute name "p1" doesn't conform to snake_case naming style - A class with too few public methods —
R0903: Too few public methods - The import order is incorrect
Disable specific warnings
You can deactivate warnings directly in the code:
# pylint: disable=invalid-name
p1 = Player("The Knight", Sword())
p2 = Player("The Dragon", FireBreath())
pylintrc configuration file
For projects with custom rules, you can generate a configuration file:
# Générer un fichier de configuration RC
pylint --generate-rcfile > pylintrc
The pylintrc file contains many configurable options. For example, to add acceptable short names:
[BASIC]
# Good variable names which should always be accepted
good-names=i,j,k,ex,Run,_,p1,p2
If you save the file under the name pylintrc, Pylint takes it into account automatically.
Pylint limits for PEP 8
Important: Pylint’s PEP 8 checks are not as complete as those of pycodestyle. For example, Pylint does not report the absence of two empty lines before a top-level class, because it does not consider this problem to be a code smell. For a more complete PEP 8 check, it is better to use Flake8.
2.6 Demo: Check your code with Flake8
Flake8 Overview
Flake8 is a wrapper around three tools:
- PyFlakes — checks for general code smells (potential problems), but not PEP 8 style problems
- pycodestyle — exclusively checks for PEP 8 style issues
- McCabe script (by Ned Batchelder) — checks for unnecessary code complexity
Flake8 does fewer things than Pylint, but it does them faster. It is more comprehensive for strict PEP 8 checks.
Installation and use
# Installation
pip install flake8
# Vérifier un fichier
flake8 game.py
# Vérifier un package entier
flake8 gamedemo/
Flake8 can detect PEP 8 issues that Pylint doesn’t show, like:
E121 continuation line under-indented for hanging indentE302 expected 2 blank lines, got 1
Configuring Flake8
You can create a .flake8 configuration file at the root of the project:
[flake8]
max-line-length = 88
exclude =
.git,
__pycache__,
build/
ignore = E203, W503
Combined approach
Many developers run Pylint AND Flake8 to get the best of both worlds:
pycodestyle(via Flake8) for PEP 8 errors- Pylint for code smells and other problems
It is also possible to use pycodestyle directly to check PEP 8 compliance only, then Pylint separately for code smells.
2.7 Demo: Correct your formatting with Black
Black Overview
Black is an automatic code formatter that has taken the Python world by storm in recent years. The difference with the other tools seen so far is that Black modifies the code: it reformats the files so that they comply with PEP 8.
Black describes itself as “the uncompromising code formatter”. His philosophy:
“By using Black, you agree to cede control over the details of hand-formatting. In return, Black gives you speed, determinism, and freedom from pycodestyle nagging about formatting.”
In other words: by using Black, you agree to cede control over the details of manual formatting. In exchange, Black offers speed, determinism, and freedom from pycodestyle’s caveats on formatting.
Installation and use
# Installation
pip install black
# Reformater un package entier
black gamedemo/
# Reformater un fichier unique
black game.py
# Aperçu des changements sans modifier le fichier
black --check game.py
# Définir la longueur de ligne maximale
black --line-length 79 gamedemo/
Particularities of Black
- Black takes very few options — that’s the point. It uses strict formatting rules, and you can’t really change them.
- All code formatted by Black will look exactly the same.
- By default, Black allows lines longer than PEP 8 (88 characters instead of 79). This can be modified with the
--line-lengthoption. - Once Black is used, no more discussion on the style: all code respects the Black style.
After formatting
After running Black, we can check if there are any warnings remaining with Flake8. In general, Black fixes almost all PEP 8 style issues, with the possible exception of lines that are too long (depending on configuration).
2.8 Demo: Use verification tools from VS Code
Enabling linting in VS Code
By default, VS Code does not show style warnings. To activate linting:
- Go to Settings
- Search for the settings of the Python extension
- Find the Enable Linting checkbox and enable it
- VS Code will offer to install Pylint (the default linter)
- It is also possible to choose another linter like Flake8 via the second button
Configuring the auto formatter
To have VS Code automatically reformat the code when saved:
- In Settings → Text Editor → check Format On Save
- In the Python extension settings, look for Formatting: Provider
- Select black (instead of
autopep8which is the default) - Install Black manually:
pip install black
Once configured, each Python file save will automatically trigger Black to reformat the code.
Enabling Mypy in VS Code
VS Code also allows enabling Mypy for type checking in the Python extension settings. Mypy will then work like Pylint or Black, displaying warnings directly in the editor.
2.9 Summary: Style tools
Here is a summary of the different tools seen in this module:
| Tool | Type | Role |
|---|---|---|
pycodestyle | Check | PEP 8 verification only (used internally by Flake8) |
Flake8 | Check | pycodestyle wrapper + PyFlakes + McCabe (fast, simple) |
Pylint | Check | Very flexible, checks code smells, style, and much more |
Black | Trainer | Automatically reformats the code (without options, very opinionated) |
reorder-python-imports | Tool | Reorganizes Python imports (something Black doesn’t do) |
Recommended approach: Most Python developers do not use these tools manually, but in automated workflows:
- CI/CD pipeline (e.g. Jenkins) that runs Pylint or Flake8 automatically
- pre-commit framework — a very popular tool that runs checks before each Git commit
# Exemple d'installation du framework pre-commit
pip install pre-commit
# Fichier .pre-commit-config.yaml
# repos:
# - repo: https://github.com/psf/black
# hooks:
# - id: black
# - repo: https://github.com/pycqa/flake8
# hooks:
# - id: flake8
3. Document your project
3.1 Introduction: Docstrings and PEP 257
Context
Let’s imagine that a first project is nearing completion, and that it is a library intended to be used by others. For these users to be able to use it easily, it requires easily accessible documentation, easy to read and search.
This module presents the toolkit used by most large Python projects to maintain their documentation and generate documents in multiple formats (PDF, HTML, etc.).
Module plan:
- A brief overview of Python docstrings and PEP 257 conventions
- Introduction to Sphinx, a tool that generates beautiful HTML documentation
- The reStructuredText format used by Sphinx
PEP 257: docstring conventions
There is a document PEP 257 on the Python documentation. It defines the conventions and semantics associated with Python docstrings — in other words, it gives guidance on how to write clear and useful documentation.
As with PEP 8, there are no strict rules. As the PEP itself says:
“If you violate these conventions, the worst you’ll get is some dirty looks.”
(If you violate these conventions, the worst that can happen is receiving disapproving looks.)
Important rules about the form of docstrings
Docstring on one line:
def ma_fonction():
"""Résumé bref de ce que fait la fonction."""
pass
Docstring on multiple lines:
def ma_fonction(param1, param2):
"""Résumé bref de ce que fait la fonction.
Description plus détaillée si nécessaire.
Peut s'étendre sur plusieurs lignes.
:param param1: Description du premier paramètre.
:param param2: Description du deuxième paramètre.
:return: Description de la valeur retournée.
"""
pass
Important points:
- The docstring starts and ends with three double quotes
""" - For a multi-line docstring, the closing quotes are on a separate line
- Closing quotes are at the same indentation level as opening quotes
- Sphinx removes indentation when generating HTML
3.2 Demo: Generate HTML documentation with Sphinx
Presentation of Sphinx
Sphinx is a Python documentation generator. It is the de facto standard for generating Python documentation, used by many large projects, including the official documentation of the Python language itself.
Input formats: reStructuredText Output formats: HTML, PDF, eBooks, UNIX man pages, and more
One of the key features of Sphinx is extracting docstrings from Python code to generate beautiful documentation.
Sphinx is very complete: if a feature is missing, there is probably an extension available.
Installation and initialization
# Installation de Sphinx
pip install sphinx
# Créer le répertoire de documentation
mkdir docs
cd docs
# Lancer le script de configuration interactif
sphinx-quickstart
The sphinx-quickstart script creates a directory structure and asks configuration questions:
- Project Name
- Author Name
- Project version
- Default language (e.g.
enfor English)
Typically, the default values can be accepted by pressing Enter.
Structure created by sphinx-quickstart
docs/
├── _build/ # Dossier où Sphinx place la sortie générée
├── _static/ # Fichiers statiques (CSS, images)
├── _templates/ # Templates personnalisés
├── conf.py # Fichier de configuration Python
├── index.rst # Fichier d'entrée (table des matières principale)
└── Makefile # Fichier Make pour générer la documentation
_build/: contains generated HTML and other outputconf.py: Python script containing all the configuration (project name, version, extensions, etc.)index.rst: main entry point in reStructuredText
Generate HTML documentation
# Depuis le dossier docs/
make html
# Pour tout régénérer depuis zéro (nettoyer puis regénérer)
make clean html
The generated documentation is in _build/html/.
3.3 Demo: Understanding reStructuredText
reStructuredText Overview
reStructuredText (also written reST or RST) is a plain text format that uses markup in a very natural way. This is the Sphinx input format.
Fundamental rules
Rule 1: Blank lines are important
All paragraphs must be separated by blank lines. Code examples are also surrounded by blank lines.
Rule 2: Indentation matters
Indentation is used to group elements together.
Sections and Headers
In reStructuredText, we create sections by underlining the text with a header. The format is intelligent: it automatically detects what is a first-level and second-level header according to the order of appearance.
Mon titre principal
===================
Section de premier niveau
--------------------------
Sous-section
~~~~~~~~~~~~
It is possible to use almost any convention for underscore characters (=, -, ~, +, etc.). You can also add an overline:
==================
Mon titre principal
==================
Sphinx will automatically determine levels based on the order in the file.
Guidelines
Directives are special instructions for Sphinx that start with .. followed by the directive name:
.. toctree::
:maxdepth: 2
introduction
chapitre1
chapitre2
The toctree directive tells Sphinx to generate a table of contents.
Code blocks
To insert a block of code, end the previous paragraph with two semicolons (::) and indent the code:
Voici un exemple de code ::
def ma_fonction():
return 42
Important: Do not forget the empty lines before and after the code block, as well as the indentation.
Bulleted lists
* Premier élément
* Deuxième élément
* Troisième élément
- Aussi une liste
- Avec des tirets
+ Et aussi avec des plus
Hyperlinks
Link with text different from URL:
`Texte du lien <https://www.example.com>`_
Simple link (text = URL):
https://www.example.com
Link to a section:
`Titre de la section`_
Comments
A paragraph starting with .. without any other directive is considered a comment, ignored during HTML generation:
.. Ceci est un commentaire et ne sera pas inclus dans la sortie HTML.
3.4 Summary: Sphinx and reStructuredText
Sphinx workflow:
# 1. Installation
pip install sphinx
# 2. Créer le dossier docs
mkdir docs && cd docs
# 3. Initialisation
sphinx-quickstart
# 4. Modifier les fichiers .rst
# 5. Générer le HTML
make html
Important notes:
- All Sphinx commands run from inside the
docs/folder - Configuration is in
conf.pyand can be edited afterwards - Configuration choices made during quickstart are stored in
conf.py
ReStructuredText rule summary:
| Element | Syntax |
|---|---|
| Paragraphs | Separated by an empty line |
| Sections | Underlined text (=, -, ~, +, etc.) |
| Lists | *, - or + at start of line |
| Code | Previous paragraph ends with :: + indentation |
| Link (full) | `Text <URL>`_ |
| Link (simple) | `Text`_ |
| Comment | .. comment |
3.5 Demo: Using reStructuredText in Docstrings
reStructuredText in docstrings
We can use the reStructuredText syntax inside docstrings Python. This helps enrich the documentation generated by Sphinx with cross-links, references to other classes and methods, etc.
Example module with enriched docstrings
"""
Gamedemo Module
===============
Module principal du jeu de démonstration.
"""
class Game:
"""Classe principale du jeu.
Gère la boucle de jeu entre deux joueurs.
Pour lancer le jeu, voir :meth:`run`.
"""
def __init__(self, player1, player2):
"""Initialise le jeu avec deux joueurs.
:param player1: Le premier joueur (ex. le chevalier).
:param player2: Le deuxième joueur (ex. le dragon).
"""
self.player1 = player1
self.player2 = player2
def run(self):
"""Lance la boucle de jeu.
À chaque tour, les joueurs s'attaquent mutuellement.
Voir aussi :meth:`~gamedemo.weapons.Weapon.attack` et
:meth:`~gamedemo.player.Player.take_hit`.
"""
while True:
# logique du jeu
pass
Cross-references in docstrings
The reStructuredText syntax allows you to create hyperlinks to other parts of the code:
| Target | Syntax |
|---|---|
| Method | :meth:`run` |
| Class | :class:`Player` |
| Function | :func:`my_function` |
| Module | :mod:`gamedemo.player` |
| Attribute | :attr:`health` |
Reference in the same module:
:class:`Player`
Reference in a different module:
:class:`gamedemo.player.Player`
Parameter documentation
def __init__(self, name: str, weapon: 'Weapon', health: int = 100):
"""Initialise un joueur.
:param name: Le nom du joueur.
:param weapon: L'arme initiale du joueur.
:param health: Les points de vie initiaux (défaut : 100).
"""
Correct indentation of multi-line docstrings
class MonExemple:
"""Titre de la documentation.
Cette description peut s'étendre sur plusieurs lignes.
L'indentation doit être au même niveau que les guillemets.
Sphinx retirera l'indentation lors de la génération HTML.
"""
3.6 Demo: Generate code documentation with APIDoc
Presentation of sphinx-apidoc
sphinx-apidoc is a command line tool included with Sphinx that scans a Python package and automatically generates reStructuredText files from the found docstrings.
Usage
The command must be executed from the project root folder (and not from the docs/ folder):
# Depuis la racine du projet
sphinx-apidoc -o docs gamedemo
-o docs: specifies where to place the generated output (thedocs/folder)gamedemo: the name of the package to document
sphinx-apidoc scans the package and all subpackages and modules it contains, then generates two files:
gamedemo.rst — contains directives to generate documentation for each module:
gamedemo package
================
gamedemo.game module
--------------------
.. automodule:: gamedemo.game
:members:
:undoc-members:
:show-inheritance:
gamedemo.player module
----------------------
.. automodule:: gamedemo.player
:members:
:undoc-members:
:show-inheritance:
modules.rst — contains a directive to generate a table of contents:
gamedemo
========
.. toctree::
:maxdepth: 4
gamedemo
Configuring conf.py
For sphinx-apidoc to work, you must configure conf.py:
1. Add the project to sys.path:
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
2. Enable the necessary extensions:
extensions = [
'sphinx.ext.autodoc', # Active la directive automodule
'sphinx.ext.viewcode', # Permet de voir le code source dans le HTML
]
Regenerate documentation
# Depuis le dossier docs/
make clean html
Option —full
It is possible to create a complete documentation project in a single command:
# Crée tout depuis zéro (dossier docs, Makefile, conf.py, etc.)
sphinx-apidoc --full -o docs gamedemo
This option allows you to completely skip the sphinx-quickstart step.
Documentation Released
Once the HTML documentation is generated, several publishing options:
- Hosting on your own web server
- readthedocs.org — can connect to the Git repository and host the HTML directly from there
- GitHub Pages — can also host Sphinx generated HTML directly from a Git repository
3.7 Summary: Code Documentation
Complete workflow with sphinx-apidoc
# 1. Depuis le dossier racine du projet
sphinx-apidoc -o docs <nom_du_package>
# 2. Depuis le dossier docs/
make clean html
Note: sphinx-apidoc runs from the project root, not from docs/.
Help and options:
sphinx-apidoc --help
Directives generated
automodule— generates documentation for a moduleautofunction— generates documentation for a function- These directives are enabled via the autodoc extension in
conf.py
Cross-references in docstrings
| Python build | Syntax reStructuredText |
|---|---|
| Class | :class:`Player` |
| Class (other module) | :class:`gamedemo.player.Player` |
| Method | :meth:`Player.take_hit` |
| Function | :func:`my_function` |
| Module | :mod:`gamedemo.player` |
4. Improve your code with type checking
4.1 Introduction: Static typing vs. dynamic typing
A common criticism of Python
There is a common criticism of Python: Python does not have static type checking, which allows you to write code that causes certain errors that you would not have if Python had static typing. Over the years, Python has added features to the language and its ecosystem to implement this using type hints.
The two main types of languages
Statically typed languages (e.g. Java, C#):
- We must declare the type of variables and function parameters
- Compiler can check types before execution
- Example in C#:
int age = 25; string name = "Alice";
Dynamically typed languages (e.g. Python):
- We do not need to declare the types
- Types are checked at runtime
- More flexible, but more prone to runtime type errors
Type hints in Python
Since Python is a dynamically typed language, type hints (type annotations) have been introduced to allow optional type checking:
- The first form of hints type was added in Python 3.5
- Development has continued since (things are still evolving)
- Type hints are totally optional and ignored by the Python interpreter
- They do not affect the behavior of the program at runtime
Mypy is the command line reference type checker. Guido van Rossum, the creator of Python, is actively involved in its development.
4.2 Demo: Type hints
Example without type hints
def average(a, b, c):
return (a + b + c) / 3
# Erreur de débutant : le deuxième argument est une string, pas un int
result = average(10, "20", 30)
# RuntimeError à l'exécution, sur la ligne (a + b + c), même si le problème est à l'appel
The problem here is that the runtime error points to the inner line of the function (line 2), not to the line of the incorrect call (line 5). It’s not very useful.
Same example with type hints
def average(a: int, b: int, c: int) -> int:
return (a + b + c) / 3
# Maintenant, le vérificateur de types signale une erreur directement à l'appel
result = average(10, "20", 30) # Erreur de type détectée ici!
With type hints, the editor (PyCharm, VS Code) can report the error directly where it occurs — when calling the function.
Variable annotations
You can also annotate variables (not just function parameters):
# Annotation d'une variable locale
x: int = 42
name: str = "Alice"
scores: list[int] = [95, 87, 92]
# Annotation d'un attribut de classe
class Player:
health: int
name: str
4.3 Understanding type hints
Key Features
- Type hints are entirely optional — Python code executes identically with or without them
- They are ignored by the Python interpreter at runtime
- You can still call functions with arguments of the wrong type and get runtime errors
- They are mainly used for tooling: editors, type checkers like Mypy
Why use type hints?
1. Error prevention Type hints allow type errors to be detected before execution, using editors and type checkers.
2. Additional documentation Type hints act as additional documentation: they specify the type of arguments expected by a function and what it returns, making the code easier to understand.
3. Publisher Support Many modern editors understand type hints and offer improved features: more precise autocompletion, real-time error detection, safer refactoring.
4. Maintainability Particularly important for large projects, where it is easy to lose track of data types.
Disadvantages
- Need to write more code
- This can become very complex if you want to annotate everything very precisely
Good practice
Type hints are optional — you don’t need to use them everywhere. For a small disposable script there is no need to add them. They are especially useful when something needs to be exactly right or in large projects.
Basic syntax
Parameter and return value annotations:
def calculer(x: int, y: float) -> float:
return x * y
def saluer(name: str) -> None:
print(f"Bonjour, {name}!")
Initialization methods:
class Player:
def __init__(self, name: str, weapon: 'Weapon') -> None:
# Pas de type de retour pour __init__, ou -> None
self.name = name
self.weapon = weapon
Note: For __init__, we generally don’t specify a return type, because it doesn’t really make sense. We can add ->None if we wish, it is equivalent to omitting it.
4.4 Demo: Apply type hints in a project
Example with the role-playing project
Added hint type health to class Player:
from gamedemo.weapons import Weapon
class Player:
def __init__(self, name: str, weapon: Weapon, health: int = 100) -> None:
self.name = name
self.weapon = weapon
self.health = health
def take_hit(self, damage: int) -> None:
self.health -= damage
def is_alive(self) -> bool:
return self.health > 0
Use in game.py
# Correct : respecte les types déclarés
knight = Player("The Knight", Sword(), 100)
dragon = Player("The Dragon", FireBreath(), 500)
# Incorrect : l'ordre des paramètres est mauvais
# Sans type hints, cette erreur passe inaperçue jusqu'à l'exécution
# Avec type hints, l'éditeur signale immédiatement l'erreur
knight = Player("The Knight", 100, Sword()) # Erreur de type détectée!
Type hints allow you to catch this type of error when writing the code, even before executing it.
4.5 Types and classes
Fundamental principle
All classes in Python are types. This includes:
- All built-in classes:
str,float,int,bool, etc. - Collections:
list,dict,set,tuple - All classes that you write yourself
- Everything that can be imported from a library
However, there are also types that are not classes.
Basic types
x: int = 5
y: float = 3.14
name: str = "Alice"
flag: bool = True
Collection types
# Liste d'entiers
scores: list[int] = [95, 87, 92]
# Ensemble d'entiers
ensemble: set[int] = {1, 2, 3}
# Dictionnaire : clés str → valeurs float
prix: dict[str, float] = {"pomme": 1.5, "orange": 2.0}
# Tuple avec types spécifiés
point: tuple[int, str, float] = (1, "label", 3.14)
# Tuple de longueur variable (tous les éléments ont le même type)
nombres: tuple[int, ...] = (1, 2, 3, 4, 5)
Note on Python versions:
- Python 3.9+: we can write
list[int],dict[str, float]directly - Python 3.8 and earlier: you must import from
typing:from typing import List, Dict→List[int],Dict[str, float]
Reference: Mypy cheat sheet
For a complete reference of type annotations, the site mypy.readthedocs.io has an excellent cheat sheet with many practical examples.
4.6 Demo: Apply type hints in a project (part 2)
The weapons module with type hints
from abc import ABC, abstractmethod
from typing import Optional
import random
class Weapon(ABC):
"""Classe de base abstraite pour toutes les armes."""
@abstractmethod
def attack(self) -> tuple[int, str]:
"""Effectue une attaque.
:return: Un tuple (dégâts, son) où dégâts est un int et son est une str.
"""
pass
class Sword(Weapon):
"""Une épée, wielded by the knight."""
def attack(self) -> tuple[int, str]:
damage = random.choice([10, 15])
sound = random.choice(["Slash!", "Clang!"])
return damage, sound
class FireBreath(Weapon):
"""Le souffle de feu du dragon."""
def __init__(self) -> None:
self._cooldown: int = 0 # Inférence de type : int (pas besoin d'annoter)
def attack(self) -> tuple[int, str]:
if self._cooldown > 0:
self._cooldown -= 1
return 0, "The dragon is cooling down..."
dice_roll: int = random.randrange(1, 7)
if dice_roll > 3:
dmg = 4 * dice_roll
sound = "ROAR! Fire!"
self._cooldown = 2
else:
dmg = 0
sound = "Just a puff of smoke..."
return dmg, sound
Type inference
The type checker can guess the type of a variable by looking at what is assigned to it. We call this type inference:
def attack(self) -> tuple[int, str]:
dmg = 0 # Le vérificateur infère : dmg est int (assignation de 0)
dice_roll = random.randrange(1, 7) # randrange retourne int, donc dice_roll est int
dmg = 4 * dice_roll # int * int = int
sound = "ROAR!" # Le vérificateur infère : sound est str
return dmg, sound # tuple[int, str] ✓
No need to annotate dmg or sound — the checker infers them correctly.
Types for lists and collections in a class
class Game:
def __init__(self, players: list[Player]) -> None:
self.players: list[Player] = players
Type Union with None
from typing import Optional
class Giant(Player):
"""Un géant qui peut ne pas avoir d'arme."""
def __init__(self, name: str, weapon: Optional[Weapon] = None) -> None:
# Optional[Weapon] équivaut à Weapon | None
self.name = name
self.weapon = weapon
Dictionary return type
def attack(self) -> dict[str, int | str]:
return {"damage": 10, "sound": "Slash!"}
# Avec Any si on ne se soucie pas des types des valeurs
from typing import Any
def attack(self) -> dict[str, Any]:
return {"damage": 10, "sound": "Slash!", "critical": True}
Important: Using Any essentially disables type checking for these values — this should be avoided unless absolutely necessary.
4.7 Special types: Union, Any and Collections
The Union type
Type Union represents one or the other of more than one type.
Modern syntax (Python 3.10+):
def ma_fonction(x: int | str) -> None:
pass
Old syntax (all versions):
from typing import Union
def ma_fonction(x: Union[int, str]) -> None:
pass
Union with None (optional type):
# Moderne
def chercher(nom: str) -> float | None:
pass
# Ancienne syntaxe avec Optional
from typing import Optional
def chercher(nom: str) -> Optional[float]:
pass
Optional[T] is equivalent to T | None.
The Any type
Any means “I have no opinion on the type”. Any value is compatible with Any.
from typing import Any
def afficher(valeur: Any) -> None:
print(valeur)
Use in a concrete example:
from typing import Any
def max_avg(data: dict[Any, list[float]]) -> list[float]:
"""Retourne la liste avec la plus grande moyenne.
Les clés du dict peuvent être de n'importe quel type (on ne les utilise pas).
Les valeurs doivent être des listes de nombres.
"""
return max(data.values(), key=lambda lst: sum(lst) / len(lst))
# Appel avec des clés de types variés (tout est accepté avec Any)
result = max_avg({
"alpha": [1, 2, 3], # clé : str
42: [4.5, 5.5, 6.0], # clé : int
True: [10.0, 20.0], # clé : bool
})
In this example, we don’t care about keys because the function doesn’t use them. Using Any for keys is therefore justified.
Important: int is compatible with float in type hints — we can pass int where float is expected.
Summary table
| Type | Description | Example |
|---|---|---|
int | Whole | age: int = 25 |
float | Floating number | pi:float=3.14 |
str | String | name: str = "Alice" |
bool | Boolean | active: bool = True |
list[T] | List of elements of type T | scores: list[int] |
dict[K, V] | Dictionary with K keys and V values | price: dict[str, float] |
| `tuple[T, …] | Tuple (fixed types) | dot: tuple[int, str, float] |
set[T] | T Type Elements Set | ids:set[int] |
T| None | Optional type (or None) | weapon:Weapon| None |
Union[T, U] | Either type (old syntax) | Union[int, str] |
Optional[T] | Equivalent to T | None (old syntax) | Optional[float] |
Any | Any type | data:Any |
4.8 Duck Typing
Duck typing concept
“If it looks like a duck and quacks like a duck, it’s a duck.”
duck typing is a very common concept in Python. We don’t care about the exact type of an object — what matters is its behavior (what methods it supports).
Problem with strict list type
import math
def compute_roots(l: list[int]) -> list[float]:
return [math.sqrt(x) for x in l]
# Fonctionne bien
result = compute_roots([1, 4, 9, 16])
# Erreur de type! range() ne retourne pas une list
result = compute_roots(range(1, 10))
One could convert range to list, but that would be unnecessary work just to satisfy the type checker.
The question is: does it really have to be a list? In fact, all we want is something that we can iterate on — a list, a tuple, a set, even a Pandas Series.
Solution: the Iterable type
from typing import Iterable
import math
def compute_roots(l: Iterable[int]) -> list[float]:
return [math.sqrt(x) for x in l]
# Maintenant, ceci fonctionne sans erreur de type
result = compute_roots(range(1, 10))
result = compute_roots([1, 4, 9, 16])
result = compute_roots((1, 4, 9)) # tuple aussi!
Iterable[T] matches anything that can be used in a for loop.
Protocols
To go even further, you can create your own protocols — types defined by their behavior:
from typing import Protocol, runtime_checkable
@runtime_checkable
class SupportsClose(Protocol):
"""Protocol pour tout objet qui supporte la méthode close()."""
def close(self) -> None:
...
class Resource:
def close(self) -> None:
print("Ressource fermée")
def close_all(items: Iterable[SupportsClose]) -> None:
for item in items:
item.close()
# Resource est de type SupportsClose car elle a une méthode close()
close_all([Resource(), Resource()])
Useful predefined protocols
There are many predefined protocols in collections.abc:
| Protocol | Required behavior | Description |
|---|---|---|
Iterable | __iter__ | Can be used in a for loop |
Sized | __len__ | Support len() |
Container | __contains__ | Supports in operator |
Collection | __iter__ + __len__ + __contains__ | Iterable + Sized + Container |
Sequence | Indexable + ordered | List, tuple, string |
Mapping | Key → value | Dict and similar types |
Callable | __call__ | Can be called as a function |
These protocols allow you to write type hints based on behavior rather than on concrete type.
Advanced topics not covered in this course
- Generics (parametric types)
- Literal types (literal values as type)
- Final values (non-modifiable values)
- Other special types for advanced uses
To find out more, refer to the Mypy documentation: mypy.readthedocs.io
4.9 Demo: Mypy
Presentation of Mypy
Mypy is the command-line reference type checker for Python. This is the project that leads the development of type hints and type checking in Python.
Advantages of Mypy over editor-integrated checkers:
- Mypy only specializes in type checking → best implementation
- As a command line program, it can be used in automated workflows
- More comprehensive: detects issues that PyCharm may miss
Installation and use
# Installation (dans un virtual environment actif)
pip install mypy
# Vérifier un fichier unique
mypy max_avg.py
# Vérifier un package entier
mypy gamedemo/
# Vérifier avec des options
mypy --strict gamedemo/
Example: error that Mypy detects but not PyCharm
Program max_avg.py:
from typing import Any
def max_avg(data: dict[Any, list[float]]) -> list[float]:
return max(data.values(), key=lambda lst: sum(lst) / len(lst))
# PyCharm ne montre pas d'erreur ici...
result = max_avg({
"test": [1.0, 2.0],
"autre": ["a", "b", "c"], # Liste de strings, pas de floats!
})
Run Mypy:
mypy max_avg.py
# max_avg.py:8: error: List item 0 has incompatible type "str"; expected "float"
Example: incompatibility with superclass
from abc import ABC, abstractmethod
class Weapon(ABC):
@abstractmethod
def attack(self) -> tuple[int, str]:
pass
class BadWeapon(Weapon):
def attack(self) -> tuple[int, str] | None: # Retour incompatible!
if some_condition:
return 10, "Slash!"
return None # Mypy détecte que None n'est pas compatible avec la superclasse
mypy gamedemo/
# gamedemo/weapons.py:15: error: Return type "tuple[int, str] | None" of "attack"
# incompatible with return type "tuple[int, str]" in supertype "Weapon"
PyCharm might not detect this incompatibility. Mypy is more sophisticated.
Mypy in VS Code
For VS Code users, Mypy can be enabled in the Python extension settings — it will then work like Pylint or Black, displaying errors directly in the editor.
Useful resources
- mypy.readthedocs.io — complete documentation, including an excellent cheat sheet with examples
- Cheat sheet — lists all available types with examples
- Predefined protocols — list of all available predefined protocols
5. Conclusion
This Python 3 Best Practices course covered three main areas to improve the quality of Python code:
General Summary
1. PEP 8 — Style and formatting
- PEP 8 is the official Python style guide
- The Pylint and Flake8 tools allow you to check compliance
- Black automatically reformats the code
- These tools integrate into editors (PyCharm, VS Code) and CI/CD pipelines
2. Docstrings and Sphinx — Documentation
- PEP 257 defines conventions for docstrings
- Sphinx generates professional HTML/PDF documentation from docstrings
- reStructuredText is the format used by Sphinx
- sphinx-apidoc automatically extracts docstrings from code
3. Type hints and Mypy — Type checking
- Type hints are optional but improve code quality
- They serve as additional documentation
- Mypy offers the most comprehensive command line type checking
- Duck typing and protocols enable behavior-based annotations
Essential Tools
# Vérification de style
pip install pylint flake8
# Formatage automatique
pip install black
# Documentation
pip install sphinx
# Vérification de types
pip install mypy
These practices allow you to write clearer, more readable, and more maintainable code, with better documentation — essential qualities for any professional Python project.
Search Terms
python · foundations · data · analysis · engineering · analytics · type · pep · hints · docstrings · documentation · mypy · types · installation · restructuredtext · presentation · pylint · rules · black · configuring · flake8 · generate · pycharm · sphinx