Ceci est le premier cours d'informatique à l'université : fondamentaux de la programmation
, enseigné avec Python 3. Il ne suppose aucune connaissance préalable en programmation. Chaque programme de
ce document est complet et exécutable — saisissez-le, exécutez-le, modifiez une seule ligne, et prévoyez
ce qui changera avant de relancer l'exécution. Cette boucle prévoir-ensuite-exécuter constitue la compétence entière.
Le cours est une échelle :
Les unités 1.1 à 1.3 construisent les mécanismes : valeurs et types, sélection, itération.
L'unité 1.4 encapsule le travail en fonctions.
Les unités 1.5 et 1.6 ajoutent les collections standards et les fichiers avec gestion des erreurs.
L'unité 1.7 représente la discipline supérieure : décomposition des problèmes, tests, débogage.
Comment lire chaque unité : une courte histoire explique pourquoi c’est important ; une image ou un principe général
viennent avant toute syntaxe ; puis les règles précises ; ensuite des exemples corrigés qui deviennent progressivement plus difficiles,
chacun avec une trace que vous pouvez vérifier en exécutant le code ; puis les erreurs réelles que les gens
commettent ; puis un résumé de trois lignes. Chaque exemple ici peut être vérifié d’une seconde manière —
en l’exécutant. Lire du code sans l’exécuter, c’est comme lire une partition musicale sans
l’entendre.
Une application de shopping affiche "4.20" — s’agit-il de texte ou d’un nombre ? Si c’est du texte, ajouter deux
les prix donnent "4.203.50" ; si c'est un nombre, il donne 8.7. Lequel se produit est décidé
selon le type, et l'ensemble de l'unité 1.1 consiste à apprendre à identifier les types avant même d'exécuter quoi que ce soit.
exécuter quoi que ce soit.
Une valeur possède un type, et le type détermine ce que vous pouvez en faire : int (nombre entier,
nombres), float (décimaux), str (texte), bool (True/False). La type(...)
elle ne modifie jamais l’ancienne valeur. L’image mentale, à conserver pour tout le cursus :
age = 21 # int
price = 4.20 # float
name = "Ada" # str
open_ = True # bool
print(type(age)) # <class 'int'>
Variables sont des noms liés à des valeurs ; l'affectation age = 22 lie le nom à nouveau —
elle ne modifie jamais la valeur précédente. L'image mentale, utile tout au long de la formation :
Les noms sont des étiquettes sur des boîtes ; les expressions lisent les boîtes et créent de nouvelles boîtes
marque un nombre à virgule flottante, car count * 3.50 promeut le résultat vers un float. Surveiller quel type sort
avec count = 6 ouvre la boîte int 6 et la boîte float 3.50, et la règle de mélange de Python
promeut la int — le résultat est la float21.0, jamais la int 21.
La conversion est explicite : int("21") est 21, mais "4" + 2 est un TypeError — Python
refuse de deviner si vous vouliez dire "42" ou 6. Utilisez print(...) pour la sortie ; entrez avec
input(...), qui retourne toujours une chaîne, donc les nombres nécessitent une conversion :
Exemple résolu 1 (la caisse du café).count = int(input("Coffees? ")), puis
print("Total:", count * 3.50). À 6 cafés, cela affiche Total: 21.0 — le .0
ajoutez un int * float à l'intérieur de la boucle et laissez Python vous évaluer.
d'une expression est l'habitude même d'unité 1.1. Tracez-le à l'état : input retourne
la chaîne "6" ; int(...) ouvre une nouvelle boîte contenant le int 6 ; la multiplication
crée une nouvelle boîte float21.0 ; print l'affiche.
Exemple résolu 2 (l'accident classique, réparé). Un imprimeur de bannières ajoute un compteur
à un message :
Lancez-le et Python élève TypeError: can only concatenate str (not "int") to str —
la règle refuse de deviner. La réparation nomme explicitement la conversion :
print("Sold " + str(count) + " today"), ou la version plus propre print("Vendu", count, "aujourd'hui"), which inserts the spaces for you. The lesson to carry: + signifie addition
pour les nombres, concaténation pour les chaînes, jamais un mélange silencieux.
Erreurs courantes. Oublier que input() donne toujours une chaîne, donc
input() * 2 répète le texte plutôt que de doubler un nombre. Écrire 4.20 + 6 et
s'attendre à une réponse int — n'importe quel float dans l'expression produit un float. Se fier à
0.1 + 0.2 == 0.3 (c'est False ; arrondi binaire — unité 1.2 montre la correction).
Résumé : quatre types de base déterminent chaque opération ; les noms sont des étiquettes, l'affectation réattribue la liaison;
les expressions mélangent les types par promotion avec des flottants ; les conversions sont explicites et
input() en nécessite une.
Un tourniquet de parc à thème doit facturer différemment les enfants, les adultes et les personnes âgées — un seul
programme, plusieurs chemins. La sélection est le moyen par lequel un programme choisit : if, elif,
else exécute un bloc uniquement si une condition est satisfaite.
Âge 70, étudiant : la première branche vraie détermine le prix
La condition est n'importe quelle expression booléenne ; les comparaisons (==, !=, <, >=) et
la logique (and, or, not) la construisent. Deux règles rendent toute décision lisible :
Les branches sont testées de haut en bas ; la première branche vraie l'emporte et le reste est
ignoré.
L'indentation n'est pas décorative — elle DÉFINIT le bloc. Un espace supplémentaire change quelles
lignes appartiennent à la branche.
temp = 37.5
if temp >= 38:
print("Fever")
elif temp >= 36 and temp < 38:
print("Normal")
else:
print("Low")
Exemple corrigé 1 (le prix du cinéma). Sous 12 ou 65+ payez 5, les étudiants paient 7,
tous les autres paient 10 :
age = int(input("Age? "))
student = input("Student? y/n ") == "y"
if age < 12 or age >= 65:
price = 5
elif student:
price = 7
else:
price = 10
print("Pay", price)
L'ordre compte : la première branche vraie gagne, c'est pourquoi la vérification de l'âge précède celle
student check — a 70-year-old student should pay 5, and this ordering says so. Run
it with 70 and y to see the flowchart's leftmost path taken.
Exemple corrigé 2 (tranches de notes, évolutif). Même squelette, maintenant avec une chaîne
où chaque elif ne nécessite qu'une seule comparaison, car les tests situés au-dessus ont déjà
échoué :
score = int(input("Score? "))
if score >= 80:
band = "A"
elif score >= 70:
band = "B"
elif score >= 60:
band = "C"
else:
band = "F"
print("Band:", band)
À 79, le premier test échoue et le second réussit : Band: B. L'escalade vers
remarquer que la condition elif score >= 70 n'a pas non plus besoin de and score < 80 — atteindre cette
ligne prouve déjà que la note est inférieure à 80. Compter sur l'exécution séquentielle ainsi est ce
qui rend les chaînes courtes ; ré tester tout est la version plus longue et erronée du débutant.
version.
Floating-point comparisons need care: 0.1 + 0.2 == 0.3 is False (binary
rounding), so compare against a tolerance: abs(x - y) < 1e-9.
Erreurs fréquentes. Écrire = là où on entendait == — le bug classique, et Python
le refuse dans les conditions (SyntaxError) précisément parce qu'il est si courant. Tester
le cas particulier après le cas général, afin que le cas général remporte toujours la victoire. Utiliser
elif avec une condition qui ne peut jamais être atteinte car une branche antérieure l'a déjà
capturée.
Résumé : les conditions choisissent les chemins, le premier vrai gagne, l'indentation définit le bloc ; ordonner
les tests du spécifique au général ; les nombres à virgule se comparent par tolérance, jamais par ==.
1.3
Itération : refaire encore
Payer des factures 5,000 à la main est une carrière ; avec une boucle, c'est un programme. Itération
répète un travail : une boucle for parcourt une séquence connue ; une boucle while s'exécute tant qu'une
condition reste vraie. La question de conception est toujours : connaissez-vous le nombre de passages, ou doit-on
une condition?
total = 0
for i in range(1, 101): # 1..100 — range stops BEFORE its end
total += i
print(total) # 5050
Utilisez for lorsque le nombre de passages est connu ; utilisez while lorsque c'est une condition qui décide. Chaque
while nécessite trois éléments visibles dans le code : l'initialisation de la variable, la condition,
et une modification à l'intérieur qui tend vers la sortie — sinon il s'agit d'une boucle infinie.
tableau de traçage pour la boucle factorielle*
Le tableau de traçage est l'habitude de vérification du cours pour les boucles : suivez les variables
n = 5
factorial = 1
while n > 1:
factorial *= n
n -= 1 # the step toward exiting
print(factorial) # 120
tour par tour, d'abord sur papier. Pour la boucle factorielle :
variables round by round, on paper first. For the factorial loop:
ajoutez un (n=5, f=1) → (4, 5) → (3, 20) → (2, 60) → (1, 120) à l'intérieur de la boucle et laissez Python vous évaluer.
add a print(n, factorial) inside the loop and let Python grade you.
2 supérieur à break — le compteur de passages est inconnu, la condition décide :
7 qui est supérieur à 100 — le nombre de tours est inconnu, la condition décide :
n = 100
while True:
if n % 7 == 0:
break # found it — leave the loop now
n += 1
print(n) # 105
La trace est courte : 100, 101, …, 104 échouent tous au test ; 105 réussit et break
sort. break quitte la boucle précocement ; continue passe à l'itération suivante. Les deux sont
outils de lisibilité — une boucle qui doit parcourir jusqu'à trouver quelque chose, puis s'arrêter, lit
faut-il à break pour atteindre 5 ?
que fait 1000 pour atteindre 2000 ?
balance = 1000.0
years = 0
while balance < 2000.0: # the condition itself is the goal
balance *= 1.05
years += 1
print(years, round(balance, 2)) # 15 2078.93
Quinze ans, et la boucle s'arrête dès que la condition bascule — le tableau de trace
montrerait le solde franchissant la ligne à 2078.93, sans atterrir exactement sur 2000.
Erreurs courantes. Modifier la liste (unité 1.5) ou la variable de boucle que vous êtes
for-boucle sur. range(1, 100) lorsque 100 était prévu — la fin est exclue.
Une while dont l'étape de sortie est absente ou commentée : la condition ne bascule jamais,
et le programme se bloque.
Résumé : for pour les comptages d'itérations connus, while pour les conditions ; chaque boucle affiche le début,
la condition et l'étape ; traçage sur papier, puis impression au niveau du grade ; break et continue pour
les sorties précoces propres.
Le même calcul de prix apparaît à cinq endroits ; la règle fiscale change ; quatre des
cinq sont mis à jour. Une fonction encapsule une tâche derrière un nom afin qu'elle soit écrite
une seule fois et fiable partout : des paramètres en entrée, une valeur de retour en sortie. def
définit ; l'appel exécute.
def total_price(count, unit=3.50):
"""Return the total for count items at unit price."""
return count * unit
print(total_price(6)) # 21.0
print(total_price(6, unit=4.0)) # 24.0
La définition fournit un paramètre (count, unit) ; l'appel fournit un
argument (6). Une valeur par défaut (unit=3.50) rend un argument optionnel.
return renvoie une valeur et termine l'appel ; l'affichage n'est pas un retour — une
fonction sans return retourne None, et l'appelant qui tente une arithmétique dessus
obtient une erreur de TypeError.
Appels imbriqués : l'appelant s'interrompt jusqu'à ce que la valeur revienne
Chaque appel possède sa propre portée locale : les noms à l'intérieur sont invisibles
à l'extérieur, et disparaissent lorsque l'appel se termine. Un trace d'appels imbriqués s'affiche :
appelant en pause → destinataire s'exécute → valeur de retour atterrit chez l'appelant.
Exemple résolu 1 (la commande incluant les frais).
def add_fee(amount):
"""Return amount plus a fixed 3 pence fee."""
return amount + 3
def order_total(unit_pence, quantity):
"""Return the order total: subtotal plus the fixed fee."""
return add_fee(unit_pence * quantity)
print(order_total(250, 4)) # 1003
Le contrat dans le docstring est crucial : pence entiers entrants, pence entiers sortants. Tracé
order_total(250, 4) : sous-total 1000, add_fee retourne 1003 — et la frame
diagramme ci-dessus montre la pause et la restitution.
Exemple résolu 2 (un algorithme réel à l'intérieur d'une petite fonction, amplifié).is_prime(n) retourne True lorsque rien de 2 jusqu'à $\sqrt{n}$ ne divise n :
def is_prime(n):
if n < 2:
return False
d = 2
while d * d <= n: # only test up to the square root
if n % d == 0:
return False # found a divisor — answer known, stop
d += 1
return True
print([p for p in range(2, 30) if is_prime(p)])
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Deux habitudes de fonction se rencontrent ici : un premier return termine l'appel dès que la réponse
est connue (pas besoin de variable flag), et la limite de boucle d * d <= n provient de la
dérivation — si $n = a \times b$ avec $a \le b$, alors $a \le \sqrt{n}$, donc tester au-delà de $\sqrt{n}$
ré-teste des diviseurs déjà vus. Exécution : la liste correspond exactement aux nombres premiers inférieurs à 30.
Erreurs courantes. Imprimer à l'intérieur d'une fonction au lieu de retourner, de sorte que le résultat
ne peut pas être réutilisé. Oublier le return sur une branche — ce chemin produit silencieusement
None. Accéder aux variables locales d'une fonction depuis l'extérieur ; elles disparaissent lorsque l'appel
se termine.
Résumé : def une seule fois, appel n'importe où ; paramètres entrants, une seule valeur sortante via return;
retours précoces terminent l'appel ; les variables locales naissent et meurent avec l'appel ; les docstrings énoncent le
contrat.
La caisse d'un café a besoin des commandes de la journée par ordre, du menu par nom, d'une localisation qui
ne doivent pas dériver, et les boissons uniques de la journée. Quatre jobs, quatre conteneurs — le choix
parmi eux est la décision de conception de l'unité 1.5.
Une liste est une séquence ordonnée et modifiable : marks = [72, 85, 61], indexée
à partir de 0, découpée avec marks[1:3]. Un dictionnaire fait correspondre des clés à des valeurs :
price = {"flat white": 3.5, "tea": 2.8}, lu et écrit par clé. Un tuple
est une séquence fixe (53.1, -1.5) — idéal pour les coordonnées qui ne doivent pas changer. Une
ensemble contient des éléments uniques sans ordre : {"a", "b"}.
marks = [72, 85, 61]
marks.append(90) # [72, 85, 61, 90]
best = max(marks) # 90
average = sum(marks) / len(marks) # 77.0
Aliasing : deux noms, une liste
L'affectation ne copie jamais : a = [1]; b = a; b.append(2) rend a[1, 2] — la liste
est partagée, les noms ne le sont pas. Cet aliasing représente encore les boîtes de l'unité 1.1, maintenant
avec deux étiquettes sur une seule boîte, et cela explique aussi la règle suivante : modifier une liste tout en
itérant dessus saute des éléments — itérez sur une copie (for m in marks[:]) ou construisez une
nouvelle liste.
La méthode .get(key, 0) du dictionnaire fournit 0 pour une première observation — l'idiome qui
rend le comptage en une seule ligne. Vérifiez manuellement : "tea" apparaît trois fois dans orders,
et le dénombrement affiché le confirme.
Exemple résolu 2 (le meilleur vendeur de la journée, amplifié). En s'appuyant sur le dénombrement,
rapportez l'article le plus populaire et son nombre :
best = max(tally, key=tally.get)
print(best, tally[best]) # tea 3
L'argument key=tally.get demande à max de comparer des éléments selon leurs comptes plutôt que
alphabétiquement — la même astuce key= classe n'importe quelle collection selon n'importe quel critère. Le
chaînage est essentiel : les programmes réels enchaînent ces outils (compte, puis classement) plutôt que de
re-parcourir orders deux fois.
Choisir le conteneur est une décision de conception : ordre + doublons → liste ; recherche par
nom → dictionnaire ; groupements fixes → tuple ; unicité → ensemble.
Erreurs courantes. Accéder à tally[item] += 1 sur une clé qui n'est pas encore dans le dictionnaire
(KeyError) — .get est la solution. S'attendre à ce qu'un ensemble ou un dictionnaire mémorise l'ordre.
Supposer que b = a copie une liste ; il crée une alias vers celle-ci.
Résumé : quatre conteneurs pour quatre tâches ; l'affectation partage, jamais ne copie ; .get
rend le comptage sûr ; key= transforme le classement en une seule ligne.
Le café ferme, le courant tombe — et le total du matin ne survit que si le
programme l'a écrit sur disque. Un fichier persiste au-delà du programme. with open(path) as f: opens, closes on exit, and reads text; open(path, "w") écrit
(et vide d'abord — l'accident classique).
with open("log.txt", "w") as f:
f.write("opened\n")
with open("log.txt") as f:
for line in f:
print(line.rstrip()) # rstrip removes the trailing newline
Le bloc with est la barrière de sécurité : quoi qu'il arrive à l'intérieur, le fichier se ferme.
La lecture renvoie des lignes avec leur retour à la ligne, donc .rstrip() appartient à chaque boucle.
Les erreurs sont des valeurs que Python lève comme exceptions : FileNotFoundError,
ValueError, KeyError, TypeError. try/except intercepte un type nommé — jamais un
vide except: qui cache de vrais bugs.
Exemple résolu 1 (fichier de configuration absent). Lire une taille depuis un fichier de paramètres
qui peut être manquant, ou ne pas contenir un nombre entier :
try:
with open("settings.txt") as f:
size = int(f.read().strip())
except FileNotFoundError:
size = 12 # no file yet: a sensible default
except ValueError:
size = 12 # the file holds "big", not a number
print("Text size", size)
L'exécuter trois fois — avec 16 dans le fichier, sans fichier, et avec big dans le
fichier — et il affiche une taille exploitable à chaque fois, par trois chemins différents. C'est là la forme du code robuste : une approche sensée
plusieurs voies y menant. Gérer l'erreur que vous prévoyez, là où vous pouvez répondre
à elle ; laisser les autres s'afficher bruyamment.
**Exemple résolu 2 (une boucle d'entrée robuste, escalated). Demander jusqu'à ce que l'utilisateur tape un
nombre entier :
Alimenter avec x puis 4 : le premier passage lève une exception, est capturé, affiche le message, et le
while True demande à nouveau ; le second passage réussit avec int(...) et return sort
de la boucle et de la fonction en un seul geste. Trois unités se combinent ici — la boucle (1.3),
le contrat de fonction (1.4) et l'exception (1.6) — ce qui constitue le point de l'
escalade.
Erreurs courantes. ⟨open(path, "w")⟩ sur le mauvais chemin : le fichier est créé ou
vidé avant que vous ne vous en rendiez compte. Capturer ⟨Exception⟩ de manière large et continuer avec un état
corrompu. Oublier ⟨.rstrip()⟩, ce qui fait apparaître des lignes doublement espacées dans la sortie.
Résumé : ⟨with open(...)⟩ pour la lecture et l'écriture ; ⟨"w"⟩ vide d'abord ;
les exceptions sont des valeurs — capturez le type nommé que vous attendez, réagissez de manière sensée, laissez
les autres s'afficher.
Deux équipes écrivent la même application ; l'une la livre en une semaine, l'autre s'enfonce pendant un mois. La
différence ne tient pas à la vitesse de frappe — c'est que le code de l'équipe rapide est construit à partir de petits
morceaux dont chacun peut être prouvé correct individuellement, tandis que l'équipe lente déborde une application entière d'un coup.
L'Unit 1.7 est cette discipline.
La décomposition divise un problème en fonctions d'une seule tâche chacune — chaque morceau est
assez petit pour être testé seul. Le test compare la sortie réelle d'une fonction avec celle
attendue, aux limites : zéro, un, vide, négatif, immense.
La boucle pilotée par les tests : rouge, vert, refactor
Un assert assert qui ne se déclenche jamais est un test passant. Écrire les tests avant le code lorsque possible — ils constituent le contrat exécuté, et la boucle ci-dessus
(rouge → vert → refactor) est cette habitude en action continue.
(rouge → vert → refactoring) c'est cette habitude en exécution continue.
1% sur £order_total — le test est écrit en premier, à la nouvelle limite :
Vérifier manuellement le test du milieu : subtotal 10, dix pour cent de réduction donne 10, et
Vérifiez le test central à la main : subtotal 1003, dix pour cent de réduction donne 902.7, et
int(...) arrondit vers le bas à 902. Remarquez aussi le premier assert : à 200 pence et quatre
tasses, le total reste 803 — les tests aux limites prouvent à la fois que la remise est appliquée
et qu'elle ne s'active pas prématurément.
Exemple résolu 2 (bisecter un bug avec des prints, escaladé). Le décompte à partir de 1.5
imprime {'tea': 3} mais la ligne du best-seller imprime la mauvaise boisson. La méthode :
(1) reproduire avec le plus petit cas échouant ; (2) afficher les valeurs à la limite
entre la partie fiable et la partie suspecte — ici, le comptage avant l'impression max ;
(3) corriger une chose ; (4) relancer les tests. Déplacer l'impression à mi-chemin du code
à chaque exécution divise par deux la zone de recherche : quatre impressions permettent de trouver un bogue dans un programme de seize lignes, malgré
sa capacité à se cacher.
Déboguer consiste à identifier l'écart entre ce que l'on pense que fait le code
et ce qu'il fait réellement — l'impression au seuil de confiance/suspicion est l'outil.
Erreurs courantes. Tester uniquement le chemin heureux : les limites (zéro, un, vide,
négatif) sont où les programmes plantent. Écrire une semaine de code avant le premier test,
puis déboguer une montagne. Corriger deux choses à la fois, de sorte que ni l'un ni l'autre résultat ne soit
d'un autre.
Summary : décomposer jusqu'à ce que chaque composant teste seul ; valider les limites dès qu'une
fonction existe ; écrire le test en amont si possible ; déboguer par dichotomie avec des impressions
aux seuils de confiance — l'habitude sur laquelle repose toute la formation universitaire.
Leçons interactives sur ce sujet
Traversez-le étape par étape, avec des exercices à vérification instantanée.