Intermediate

Python 3 Best Practices

At the end of this course, you will be able to write clearer, more readable, and more maintainable code, with better documentation.

Level: Intermediate Prerequisites: Basics of Python programming


Table of Contents

  1. Course Overview
  2. Follow Python style conventions: PEP 8
  1. Document your project
  1. Improve your code with type checking

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:

  1. Imports from the standard library
  2. Imports from third-party libraries
  3. 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:

ElementAgreementExample
ModulesShort, all lowercasegame, player
ClassesCapWords (without underscores)GamePlayer
Functions/methodsLowercase letters with underscoresget_player_name()
VariablesLowercase letters with underscoresplayer_health
ConstantsAll capsMAX_HEALTH = 100
Internal usePrefixed 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 class
  • PEP 8: W191 indentation contains tabs — using tabs instead of spaces
  • PEP 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 CodeReformat Code

To reorder the imports:

  • Menu CodeOptimize 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:

RuleValue
Indentation4 spaces per level
Maximum line length79 characters
Lines between classes/functions (top)2 empty lines
Lines between methods (in a class)1 empty line

Organization of imports:

  1. Standard library
  2. Third-party libraries (e.g. requests)
  3. 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 docstringsC0114: Missing module docstring
  • Names too shortC0103: Attribute name "p1" doesn't conform to snake_case naming style
  • A class with too few public methodsR0903: 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 indent
  • E302 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-length option.
  • 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:

  1. Go to Settings
  2. Search for the settings of the Python extension
  3. Find the Enable Linting checkbox and enable it
  4. VS Code will offer to install Pylint (the default linter)
  5. 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:

  1. In SettingsText Editor → check Format On Save
  2. In the Python extension settings, look for Formatting: Provider
  3. Select black (instead of autopep8 which is the default)
  4. 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:

ToolTypeRole
pycodestyleCheckPEP 8 verification only (used internally by Flake8)
Flake8Checkpycodestyle wrapper + PyFlakes + McCabe (fast, simple)
PylintCheckVery flexible, checks code smells, style, and much more
BlackTrainerAutomatically reformats the code (without options, very opinionated)
reorder-python-importsToolReorganizes 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:

  1. A brief overview of Python docstrings and PEP 257 conventions
  2. Introduction to Sphinx, a tool that generates beautiful HTML documentation
  3. 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. en for 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 output
  • conf.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

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.py and can be edited afterwards
  • Configuration choices made during quickstart are stored in conf.py

ReStructuredText rule summary:

ElementSyntax
ParagraphsSeparated by an empty line
SectionsUnderlined text (=, -, ~, +, etc.)
Lists*, - or + at start of line
CodePrevious 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:

TargetSyntax
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 (the docs/ 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 module
  • autofunction — generates documentation for a function
  • These directives are enabled via the autodoc extension in conf.py

Cross-references in docstrings

Python buildSyntax 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, DictList[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

TypeDescriptionExample
intWholeage: int = 25
floatFloating numberpi:float=3.14
strStringname: str = "Alice"
boolBooleanactive: bool = True
list[T]List of elements of type Tscores: 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 Setids:set[int]
T| NoneOptional 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]
AnyAny typedata: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:

ProtocolRequired behaviorDescription
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
SequenceIndexable + orderedList, tuple, string
MappingKey → valueDict 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.