Table des matières
The objective of this lab is to explore some aspects of the Python language (files, modules, testing, typing, and map display) through an exercise centered on the metro.
1. Introduction
Public transportation data is now often available as “open data” to encourage the development of innovative applications by entities other than the operators of these transportation networks.
For example, data describing public transportation services in the Île-de-France region is available here. This dataset is large because it covers many modes of transportation (metro, RER, bus, tram, etc.) and includes a wide range of information, such as schedules.
This exercise will focus on a simplified version of the metro network; the data has been filtered to retain only the relevant information: metro line names, and the names and geographic locations of the stations on those lines.
2. Project setup
- Create an empty folder that will serve as the project root.
- Open this folder in VSCode.
- Create an environment and activate it.
- Create a folder named
dataand save the data file in this folder.
The columns in this file are: line_name, station_name, stop_lat, stop_lon; the values in each line are separated by commas; the first two columns are strings, and the last two are floating-point numbers.
The information available is as follows:
- Metro stations: they are identified by a name and contain stops.
- Metro lines: they are identified by a name and consist of an ordered list of stops.
- Stops: they have the name of the station they are located in, as well as geographic coordinates. The order of the stops in the file is the same as the order of these stops on the metro line.
3. Loading Data
- Create a folder named
metro_networkthat will contain the module responsible for reading the data file and making the information available. - Create the classes
MetroNetwork,MetroStation,MetroStop, andMetroLinein a file namedmetro_network.pywithin themetro_networkfolder. - The
MetroNetworkconstructor will take the name of the network and the name of the file containing that network’s data. - An auxiliary function, called by this constructor, will be responsible for reading the data and creating the corresponding objects: you will thus determine the attributes and arguments of the constructors for the other classes.
- The
DictReaderclass from thecsvmodule will be used to read the data file.
Working with files is always complicated, regardless of the programming language used: a file must be opened before its contents can be accessed, then closed; you must also account for potential errors (such as a file that cannot be accessed) and the encoding used in the case of a text file… In Python, basic file operations are available through the io module. Other modules provide specialized functionality for specific file types, such as the csv module for “Comma Separated Values” files.
Python’s with statement, used in the example with DictReader, hides this complexity; in particular, the file will always be closed after the with statement, even if an exception is raised while the file is being used.
It should be noted that the with statement can be used with resources other than files (such as database connections, network connections, etc.).
The example using DictReader does not specify the character encoding of the data file, which can lead to reading errors; therefore, you must specify encoding="utf-8" in the open() call.
4. Writing Tests
- Install the
pytestmodule in your environment.
pytest is a library that makes it easier to write unit tests.
- Add observers to the
MetroNetworkclass for the number of stations (number_stations) and the number of lines (number_lines). - Create a subfolder named
testsin the root folder. - Create a file named
test_metro_data.pyin this new folder with the following content:
from metro_network.metro_network import MetroNetwork
def test_empty_metro_network():
network = MetroNetwork("Empty", "tests/empty_metro_stops.csv")
assert network.name == "Empty"
assert network.number_stations == 0
assert network.number_lines == 0
- Create a file named
empty_metro_stops.csvin the same folder that contains only the column names.
If you simply run pytest, an error ModuleNotFoundError: No module named ‘metro_data’ will be displayed: you must tell pytest to use the current directory for module searches, which can be done as follows:
PYTHONPATH=. pytest
- Verify that the test passes.
- Take a sample from the complete data file and write another test for that sample.
5. Type annotations
Python (like JavaScript) is a dynamically typed language, which means that a variable (or a function parameter) can be assigned a value of any type, and it is only at runtime that a combination of variables of incompatible types is flagged as an error (for example, adding a string and a number).
Other languages (Java, TypeScript, C++, etc.) are statically typed: you specify the type of variables in the code (for example, you declare that a variable will hold integers), and any attempt to store data of an incompatible type in it will be detected as an error even before the code is executed. This approach reduces the “dynamic” nature of these languages but offers the advantage of detecting errors earlier in the development cycle.
In Python, it is possible to add type annotations to the code and use tools that verify, before execution, that any assignment of a value to a variable conforms to the specified type.
For example, the MetroStop constructor receives the geographic location of the stop: is this data in the form of strings, as read from the CSV file, in which case the MetroStop constructor would need to convert it to a floating point number, or is this data already in the form of numbers, in which case the caller of the MetroStop constructor would need to perform the conversion?
We’re choosing the second option here, because the stop points could be created from a database (rather than a CSV file), with geographic coordinates stored directly as numbers.
- Create a file named
test_type_annotation.pywith the following content:
def test_type_annotation(an_integer, a_string):
return "".join(a_string for _ in range(an_integer))
print(test_type_annotation(3, "abc"))
print(test_type_annotation("abc", 3))
- Run this script; you’ll get an error:
TypeError: ‘str’ object cannot be interpreted as an integer. - Modify the file
test_type_annotation.pyto add type annotations:
def test_type_annotation(an_integer: int, a_string: str) -> str:
return "".join(a_string for _ in range(an_integer))
print(test_type_annotation(3, "abc"))
print(test_type_annotation("abc", 3))
You can verify that these annotations are accepted by Pylint because they are included in the language's syntax definition (see typing. Running the script gives the same error.
To enable verification of these annotations before running the code, you need to install an additional tool: add the MyPy Type Checker extension to VSCode. After enabling this extension, VSCode will flag the error Argument 1 to "static_type" has incompatible type "str"; expected "int".
- Add type annotations to the
MetroData.pyfile:- Use
-> Nonefor a function or method that returns nothing. - You can use
floatas a type; you can also useint | floatto indicate that both are possible. - A dictionary is annotated with
dict[key_type, value_type]. - Do not annotate
selfor the return value of__init__(). - A loop variable must be annotated beforehand, for example:
- Use
row: dict[str, str]
for row in reader:
- The MyPy documentation is available here
If your Python's version is not at least 3.14, add the following line to your file to avoid a NameError:
from __future__ import annotations
6. Display
Displaying maps has become commonplace since the advent of smartphones. Leaflet is a widely used JavaScript library for this purpose. Folium is a Python library that uses Leaflet for rendering.
Maps created by Folium can be displayed in a Jupyter Notebook, saved as an HTML file, or displayed directly in a browser via a web server. We’ll use the latter option via Flask, a lightweight Python web micro-framework.
- Install Folium and Flask in your Python environment.
- Create a folder named
displayin your project folder. - Create a file named
display_network.pyin this folder with the following content:
import folium
from flask import Flask
from metro_network.metro_network import MetroNetwork
def create_map_for_network(network: MetroNetwork) -> folium.Map:
map: folium.Map = folium.Map((48.86, 2.35), zoom_start=13, tiles=None)
folium.TileLayer("OpenStreetMap", overlay=True).add_to(map)
folium.LayerControl().add_to(map)
return map
app = Flask(__name__)
@app.route("/")
def display_map():
return metro_map.get_root().render()
metro_map = None
if __name__ == "__main__":
metro_map = create_map_for_network(MetroNetwork("Metro IdF", "data/idf_metro_stops.csv"))
app.run()
- Run your application using the following command:
PYTHONPATH=. python display/display_network.py
- Open a browser window to the specified URL and verify that the map of the Paris region is displayed correctly; note in particular the control in the upper-right corner for the displayed layers.
We’ll add a layer to this initial map that will contain the line routes (we’ll limit this to line segments between stops) and another layer to display the stations.
- Define a function
create_station_layer(network: MetroNetwork) -> folium.FeatureGroupresponsible for creating a layer to display the stations; this function will need to query the network object, which will require you to add getter methods to your classes in themetro_network/metro_network.pyfile. - Call this function in
create_map_for_network()before adding theLayerControl.
To make the display easier to read, you can use different colors for the folium.Marker representing stations (black for multi-line stations; otherwise, the color chosen for that line).
The following colors are available:
['red', 'blue', 'green', 'purple', 'orange', 'darkred',
'lightred', 'beige', 'darkblue', 'darkgreen', 'cadetblue',
'darkpurple', 'white', 'pink', 'lightblue', 'lightgreen',
'gray', 'black', 'lightgray']
- Define a function
create_line_layer(network: MetroNetwork) -> folium.FeatureGroupthat creates a layer for displaying lines.

