Advanced Functions: Parameters and Annotations

First lesson of the Intermediate Python (Functions, Modules, OOP) course, free to read: about 15 minutes, 3 exercises and 3 quiz questions. This is the catalogue's base lesson; in the student space, it is rewritten for your profile.

Objectives

  • Use *args and **kwargs to accept a variable number of arguments
  • Enforce keyword-only arguments and avoid the mutable default value trap
  • Annotate parameter and return types

Advanced parametersType annotations

Advanced functions: parameters and annotations

You know how to write a function with parameters and a return. Let’s look at the tools that make functions more flexible, safer and more readable.

A variable number of arguments: *args

def total(*montants):
    """Add up as many amounts as you like."""
    # montants is a tuple containing all the positional arguments
    return sum(montants)

print(total(10, 20))          # 30
print(total(5, 5, 5, 5))      # 20
valeurs = [1, 2, 3]
print(total(*valeurs))        # 6: * unpacks the list into arguments

The name args is a convention; it is the star that matters.

Named options: **kwargs

def creer_utilisateur(nom, **infos):
    # infos is a dictionary of the extra keyword arguments
    fiche = {"nom": nom}
    fiche.update(infos)
    return fiche

print(creer_utilisateur("Lina", ville="Lille", age=31))
# {'nom': 'Lina', 'ville': 'Lille', 'age': 31}
options = {"ville": "Brest"}
print(creer_utilisateur("Yanis", **options))   # ** unpacks a dictionary

The order in a signature is always: regular parameters, *args, keyword parameters, **kwargs.

Forcing keyword arguments

A lone * in the signature forces you to name the parameters that follow. This is valuable for boolean options, which are unreadable when positional:

def exporter(donnees, *, separateur=";", entete=True):
    ...

exporter(lignes, separateur=",")   # clear
# exporter(lignes, ",", False)     # TypeError: keyword arguments required

The mutable default value trap

A default value is created only once, when the function is defined. With a list, it is therefore shared between calls:

def ajouter(article, panier=[]):      # AVOID
    panier.append(article)
    return panier

print(ajouter("pomme"))   # ['pomme']
print(ajouter("poire"))   # ['pomme', 'poire']: surprise!

def ajouter(article, panier=None):    # correct version
    if panier is None:
        panier = []
    panier.append(article)
    return panier

Type annotations

Annotations indicate the expected types. Python does not check them at runtime, but your editor and tools like mypy use them to detect errors, and they document the code:

def moyenne(valeurs: list[float], arrondi: int = 2) -> float | None:
    """Return the average, or None if the list is empty."""
    if not valeurs:
        return None
    return round(sum(valeurs) / len(valeurs), arrondi)

Since Python 3.9, you write list[float] or dict[str, int] directly; since 3.10, float | None means “a float or None”. Since Python 3.14, annotations are only evaluated when you inspect them: a function can be annotated with a class defined further down in the file, without quotes (before 3.14, this caused a NameError).

Key takeaways

  • *args → tuple, **kwargs → dictionary; * and ** also unpack in a call.
  • A lone * makes the following parameters keyword-only.
  • Use None as the default value for mutable objects.
  • Annotate your functions: it is documentation that tools can verify.

Exercises

Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.

  1. CodeDifficulty 2Python in the browser2 hints

    Flexible weighted average

    Write moyenne(*notes, arrondi=1) that returns the average of the grades passed as arguments, rounded to arrondi decimal places. If no grade is given, return None. Annotate the function.

    def moyenne(*notes: float, arrondi: int = 1) -> float | None:
        ...
  2. CodeDifficulty 2Python in the browser2 hints

    Fix the mutable default

    The ajouter_tag function below shares its list between calls. Fix it so that each call without tags starts from a fresh empty list, and make unique keyword-only (with *). If unique is True, the tag is not added if it is already present.

    def ajouter_tag(tag, tags=[], unique=False):
        if unique and tag in tags:
            return tags
        tags.append(tag)
        return tags
  3. Multiple choiceDifficulty 11 hint

    Contents of kwargs

    With def f(a, *args, **kwargs): return kwargs, what does f(1, 2, 3, x=4) return?

    • (2, 3)
    • {'x': 4}
    • {'a': 1, 'x': 4}
    • [2, 3, 4]

End-of-lesson quiz

  1. What happens if you call f(1, 2) with def f(a, *, b=0)?

    • b is 2
    • TypeError
    • a is (1, 2)
  2. Does Python check type annotations at runtime?

    • Yes, always
    • No, they are for tools and documentation
    • Only for int
  3. Why write panier=None rather than panier=[]?

    • It is faster
    • The default list would be shared between all calls
    • [] is forbidden as a default

The rest of the course

  1. Functions as Objects, Lambdas and Generators20 min
  2. Modules, Packages and the Standard Library15 min
  3. Classes and Objects20 min
  4. Inheritance, Dataclasses and Custom Exceptions20 min

Intermediate Python (Functions, Modules, OOP) · Intermediate · 3 h