Masters in Data Science applied to agricultural and food sciences, environment, and forestry engineering.
Instructor: Manuel Campagnolo (mlc@isa.ulisboa.pt)
Teaching assistant: Hannah Nathanson (hnathanson@edu.ulisboa.pt)
Revised class schedule: class (Friday, room 48) will start at 11:45 and end at 14:45, with a ~45 minute break for lunch.
* You need to self-register for the course in Moodle at <https://elearning.ulisboa.pt/user/index.php?id=12863>. The Moodle site allows you to access assignments; submissions; detailed grades and comments * If you don't have one already, create a GitHub account at <https://github.com/signup> * [CS50P: CS50’s Introduction to Programming with Python](https://cs50.harvard.edu/python). Lectures (videos and notes), problems sets, shorts; * [CS50 codespace](https://cs50.dev/). VScode codespace that allows you to use a customized AI bot (CS50's duck debugger, or DDB for short) and test your solutions for CS50P proposed problems (you can access the codespace with your GitHub user). * [Python tutor](https://pythontutor.com/). Python Tutor is a online tool that lets you write code in a web browser and see what happens step-by-step as the computer runs it. * [Fenix webpage for the course](https://fenix.isa.ulisboa.pt/courses/intpy-564938523285845): administrative information and final grades * [Help me](https://1drv.ms/o/c/0bedfe7ffa5a0eb8/IgA-CgpLN8IVTJsDg9FJ9m67AQrx95SYxoXvGRBOMDVRwjI): general help, assignements</details> ---Advanced exercises, alternative tutorials and reference materials
- Codewars is an online educational platform used to practice, improve, and master programming skills through gamified coding challenges in Python and many other programming languages. You can log in with your GitHub account and choose your proficiency level. Codewars will propose exercises for your level.
- Learn Python with Scrimba, Olof Paulson: interactive lectures (videos), examples and exercises
- Introduction to Python (VScode): interactive step by step lectures and exercises, real-time quizzes.
- Basic concepts and features of the Python language and system: The Python Tutorial at python.org.
- Python Programming course at PP.fi: same features as CS50 but to test your solutions to problems you are required to pass previous tests
- If you curious about the story of Python, check out this video. In this video, you’ll learn about the output of
>>> import thisand many other anecdotes about Python.</blockquote>
---
Class 1 (September 11, 2026): data types, variables, lists, functions
1. The recommendation for this class is to code using the CS50 codespace. Two steps: 1. log in into your GitHub account; 2. access your code space at <https://cs50.dev/>. This environment allows you to test automatically your scripts for the CS50 problem sets and gives access to a customized AI bot (DDB) 2. Some useful keyworks for the command line interface (CLI): * `code filename.py` to create a new file * `ls` to list files in folder * `cp filename newfilename` to copy a file, e.g. `cp ..\hello.py farewell.py` (`..` represents parent folder) * `mv filename newfilename` to rename or move file, e.g. `my farewell.py goodbye.py` or `mv farewell.py ..` (move one folder up) * `rm filename` to delete (remove) file * `mkdir foldername` to create new folder * `cd foldername` change directory, e.g. `cd ..` * `rmdir foldername` to delete folder * `clear` to clear terminal window 3. Typing `python` gives you access to a REPL (interactive Read-Eval-Print-Loop) environment (<https://realpython.com/interacting-with-python/>). 4. All values in Python have a **type**. The **primitive data types** are: integer, float, string, Boolean, and None (see <https://www.geeksforgeeks.org/python/primitive-data-types-vs-non-primitive-data-types-in-python/>). There are also **Non-Primitive Data Types** like `list`, `tuple`, `set` and `dictionary`. * strings (`str`), variables, print (a function), parameters (e.g. `end=`), input, comments, formatted strings (`f"..."`), `.strip()`, `.title` (methods) * integers (`int`), operations for integers, casting (e.g. `str` to `int`) * floating point values (`float`), round, format floats (e.g. `f"{z:.2f}`) * Boolean: `True`, `False`, that can be combined with operators `and`, `or`, `not` 5. Functions, `def`, `return`. Example: ``` def square(x): return x*x ``` 6. View the step-by-step execution of this code on [Python Tutor](https://pythontutor.com/visualize.html#code=def%20main%28%29%3A%0A%20%20%20%20x%3D4%0A%20%20%20%20y%3Dsquare%28x%29%0A%20%20%20%20print%28f%22%7Bx%7D%20squared%20is%20%7By%7D%22%29%0A%0Adef%20square%28n%29%3A%0A%20%20%20%20s%3Dn*n%0A%20%20%20%20return%28s%29%0A%0Amain%28%29&curInstr=12&mode=display&origin=opt-frontend.js&py=311). Identify the scope of the variables. Try to add `print(s)` in line 4 after calling function `square` inside the `main` function: why do you get a `NameError`? 7. Suggested problems: [CS50 Problem set 0](https://cs50.harvard.edu/python/psets/0/). Find a solution to the problems at your CS50 codespace, and dialog with the AI bot (DDB) for help. The bot won't produce code but will help you by answering your to questions: very precise prompts originate more details in the answers. Resist the temptation of looking for a solution on the web or using a generic AI bot.---
Class 2 (September 18, 2026): conditionals, lists, dictionaries
1. See: - [notes on CS50P lecture 1](https://cs50.harvard.edu/python/notes/1/): conditionals `if`, `match`, boolean expressions, etc - [notes on CS50P lecture 2](https://cs50.harvard.edu/python/weeks/2/): `for` and `while` loops, etc 3. Conditionals: - `if`, `elif`, `else`: ```Python if score >= 70: print("Grade: C to A") elif score >= 60: print("Grade: D") else: print("Grade: F") ``` - `match`: ```Python match species: case 'versicolor': label=0 case 'virginica': label=1 case _: label=2 ``` 3. Pythonic coding: `def main()`, define other functions, call `main()`. The code must be modular. 4. While loops, for loops, `break`, `break` and `return` ``` s='intro to python' for i in range(15): print(s[i]) ``` ``` # require user to provide a positive number while True: n = int(input("What's n? ")) if n > 0: break ``` 6. Data type *list* `[]`: methods `append`, `extend` 7. Data type *dictionary* `{}`, `items()`, keys `.key()` and values `.values()` ```Python knights = {'gallahad': 'the pure', 'robin': 'the brave'} for k, v in knights.items(): print(k, v) if 'gallahad' in knights: print('Go Gallahad') ``` 8. Suggested problems: [CS50 Problem set 1](https://cs50.harvard.edu/python/2022/psets/): Deep Thought, Home Federal Savings Bank, Math Interpreter, Meal Time 9. Assignment #1 available on Moodle: problems [File extensions (set 1)](https://cs50.harvard.edu/python/2022/psets/1/extensions/), [Coke machine (set 2)](https://cs50.harvard.edu/python/2022/psets/2/coke/), [Plates (set 2)](https://cs50.harvard.edu/python/2022/psets/2/plates/)
Class 3 (September 25, 2026): list and dictionary comprehensions, collaborative exercise
1. See lecture on [List and dictionary comprehensions](https://cs50.harvard.edu/python/shorts/list_dictionary_comprehensions/) 2. [Exercises on list comprehension (with some solutions)](/greends-ipython/docs/exercises_list_comprehension.html) 2. [Exercises on dictionary comprehension (with some solutions)](/greends-ipython/docs/exercises_dict_comprehension.html) 3. Collaborative project: each student or small group of students should define each necessary function to complete the script below. The side effect of `main()` is a simple histogram printed in the terminal. ```Python from read_values import read_values from determine_min_max import determine_min_max from number_of_classes_sturges import number_of_classes_sturges from amplitude import amplitude from determine_frequency import determine_frequency from print_frequency import print_frequency def main(): # read values x=read_values() # x is a list of numbers, either integers or floats n=len(x) # integer; number of values # determine min value and max value xmin,xmax=determine_min_max(x) # integers or floats # determine number of classes m=number_of_classes_sturges(n) # m is a positive integer such that 2**(m-1) <= n <= 2**m # determine class amplitude delta=amplitude(xmin,xmax,m) # positive float, the range of values divided by the number of classes # Compute frequency for each class and plot histogram row by row for i in range(m): left=xmin+i*delta if i < m-1: right=left+delta else: right=left+delta+1 # either 1 or any positive value freq=determine_frequency(x,left,right) # integer; note that each value must belong to one and only one class print_frequency(freq) # the output must be '****' where each * represents one observation # execute main main() ``` --- To share code, students should submit a *pull request* on GitHub. Test how to perform a pull request with a trivial `run()` function for each team: ```text 1. Fork the course repository `https://github.com/isa-ulisboa/greends-ipython` on your GitHub account 2. In your fork, go to `collaborative_proj/submissions/`. 3. Create a file called `team_name.py` with your own team name, say `team_01.py` 4. Add a `run()` function to the file with contents like `print('team 1')` 5. Commit the file. 6. Click Contribute → Open pull request. 7. Make sure it goes from your fork to the teacher’s original repository `https://github.com/isa-ulisboa/greends-ipython` 8. Click `Create pull request`. ``` If the teacher's original repository changes after you forked it (for instance, the README is updated or other teams' pull requests are merged), your fork does not update automatically. Before working on a new submission, bring your fork up to date: ```text 1. Open your fork on GitHub (https://github.com//greends-ipython). 2. If it says "This branch is X commits behind isa-ulisboa/greends-ipython:main", click Sync fork → Update branch. 3. If you also have a local clone of your fork, run `git pull` in it to get the updates. ``` Alternatively, from a local clone of your fork, using the command line: ```bash git remote add upstream https://github.com/isa-ulisboa/greends-ipython.git # only the first time git fetch upstream git merge upstream/main git push origin main ``` </blockquote> </details> --- ---
Class 4 (October 2, 2026): handling exceptions in Python: catching and raising exceptions
See CS50P lecture 3, in particular the short on handling exceptions:
try,except,ValueError,KeyError.A few examples of code that can be helpful to solve problems in CS50 Problem set 3.
Example of basic use of `try-except` to catch a `ValueError`: ``` try: x = int(input("What's x?")) except ValueError: print("x is not an integer") else: print(f"x is {x}") ``` Function for requesting an integer from the user until no exceptions are caught: ``` def get_int(): while True: try: x = int(input("What's x?")) except ValueError: print("x is not an integer") else: break return x ``` For a list of Python Built-in Exceptions, besides `ValueError`, you can check https://www.w3schools.com/python/python_ref_exceptions.asp
- The fuel gauge problem (https://cs50.harvard.edu/python/2022/psets/3/fuel/)
To solve this problem, try to organize your code as follows. As suggested in *hints*, you should catch `ValueError` and `ZeroDivisionError` exceptions in your code. In the code below, the user is being asked for correct values for `x,y` until they satisfy the requirements: `x,y` must be inputted as a string `x/y`, `x` has to be less or equal to `y`, and `y` cannot be zero. The function `get_string_of_integers_X_less_than_Y` in the code below should take care of that. ``` def main(): # asks user for input until the input is as expected x,y=get_string_of_integers_X_less_than_Y() # compute percentage from two integers p=compute_percentage(x,y) # print output print_gauge(p) ```Exercise: adapt the code proposed in the short to be more modular, where the main function is something like the one below: ``` def main(): spacecraft = input("Enter a spacecraft: ") au=get_au(spacecraft) m = convert(au) print(f"{m} m") ```
- Other useful applications of try-except
- We may want to exit the execution of our script if some exception is caught. This can be done with `sys.exit()`, which can also be used to print a message. ``` import sys # import module try: x = int(input("What's x?")) except ValueError: sys.exit("x is not an integer") ``` - Example of code that catches `CRTL-C` or `CRTL-D`: ``` while True: try: x=int(input()) except ValueError: print('x is not integer') except KeyboardInterrupt: #CTRL-C (in Linux, interrupt execution) print('\n KeyboardInterrupt') break except EOFError: #CTRL-D (in Linux, log out terminal/end-of-file) print('\n EOFError') break else: print(x) ```
- Exercise (Asking for an haphazard list of numbers): Create a program that asks the user to provide haphazardly a series of numbers that you want to store in a list. The user is asked for a number at the time. Only inputs that are numbers are stored in the list. When the user wants to stop, it should type CTRL-D. Then, the program should print the list of numbers.
---
Class 5 (October 9, 2026): modules, packages, APIs
See lecture https://cs50.harvard.edu/python/weeks/4/
Modules and packages
Suggestion: watch <https://cs50.harvard.edu/python/shorts/creating_modules_packages/> Modules are just python scripts (files like `module_name.py`) which can be imported into your main code and that can be shared between multiple applications. You can import everything that belongs to the module, or just some given function(s) or other objects.As explained on the recommended video, a python **package** is just a folder with modules and a special file named `__init__.py`Create and import your own module
Exercise: Create file named
utils.pyand filemain.pyin http://cs50.dev. Organize the files in the following folders:|--- class_5 # or whatever folder name you wish |--- common # General reusable code used across the project |--- __init__.py # optional: marks the folder as a package |--- utils.py |--- main.pyThe contents of
utils.pyare typically functions or constants that you can re-use in different contexts. Let’s suppose thatutils.pyhas the following contents.utils.py
import sys Constants={ 'e' : 2.718281828459045, # Euler's constant 'pi' : 3.141592653589793, # Archimedes' constant 'phi' : 1.618033988749895 # Golden ratio } def get_integer() -> int: #get integer from user while True: try: return(int(input('Type a number: '))) except ValueError: print('Not an integer number: try again') except KeyboardInterrupt: #CTRL-C print('\n If you want to exit type CTRL-D') except EOFError: # CTRL-D sys.exit('\n Exit as requested')and
main.pyis the following file:main.py (1st version)
import common.utils def main(): x=common.utils.get_integer() print(x) main()Run
main.pyfrom inside theclass_5folder (cd class_5, thenpython main.py), so that Python can find thecommonfolder.If you prefer, you can explicitly import some given functions from the module as in the following example.
main.py (2nd version)
from common.utils import get_integer def main(): x=get_integer() print(x) main()You can also import everything from the module with
from common.utils import *instead of the more specific (and recommended)from common.utils import get_integer.The examples above follow the directory tree that was suggested. If you change the module’s location, you need to adapt the code accordingly. In alternative, you can add the path to the directories where your modules lies to
sys.pathas in the following example.import sys sys.path.append(r'path-to-folder') # folder that contains utils.py (e.g. `/workspaces/8834091/class_5/common`) import utilsPip install
Some modules, like
sys,mathorrandom, are part of the Python standard library: they come with Python and can be imported without installing anything. For instance,randomprovides a series of functions for sampling, shuffling, and extracting random numbers from a variety of probability distributions.Many other packages are available at the Python Package Index, https://pypi.org/. They need to be installed before they can be imported, which is done in the terminal with
pip. For instance, the packagerequests(used below to access APIs) is installed with$ pip install requestsand then imported in your script with
import requests. If you want to know the folder where a module is located, you can get that information with the attribute__file__, as in the following script. Compare the two folders: the standard library and the installed packages (site-packages) are in different places.import random import requests print(random.__file__) print(requests.__file__)Suggestion: write a script to estimate the value of $\pi$ with a Monte Carlo algorithm that makes calls to
random.uniform(-1, 1). One possible solution: https://www.geeksforgeeks.org/dsa/estimating-value-pi-using-monte-carlo/
sys.argvPreviously, we used module `sys`, in particular functions `sys.exit()` and `sys.path`. Another useful function is `sys.argv`, that allows you to have access to what the user typed in at the command line `$` as in the following script. ```Python import sys print(len(sys.argv)) # returns the number of words in the command line after $python print(sys.argv[1]) # returns the 2nd word, i.e., the first word after $python myscript.py ``` For instance, the following script named `sum.py` prints the sum of two numbers that were specified in the command line.To run it, you can for instance execute the command `$python sum.py 1.2 4.3` in the terminal.sum.py
# sum.py import sys try: x,y = float(sys.argv[1]), float(sys.argv[2]) print('the sum is',x+y) except IndexError: print('missing argument') except ValueError: print('The arguments are not numbers')
- APIs
Suggestion: watch https://cs50.harvard.edu/python/shorts/api_calls/ (13') *Application program interfaces* allow you to communicate with a remote server. For instance, `requests` is a package that allows your program to behave as a web browser would. Consider the following script `myrequest.py` that allows you to explore the *itunes* database (https://performance-partners.apple.com/search-api):Example: iTunes
import requests import sys try: response = requests.get("https://itunes.apple.com/search?entity=song&limit=2&term=" + sys.argv[1]) print(response.json()) except IndexError: sys.exit('Missing argument') except requests.RequestException: sys.exit('Request failed')You can then call the API from your terminal with
$python myrequest.py 'name of my favorite band'. What you see printed on the terminal is a dictionary. How can you edit your script such that the output is just a list of track names (keytrackName)? Notice that&limit=2in the search sets the number of songs that are returned by the API.Example: GBIF
You can easily adapt that code to access a different database. For instance if you want to explore the GBIF database https://data-blog.gbif.org/post/gbif-api-beginners-guide/, you can just replace the main line of code in
myrequest.pywithresponse=requests.get('https://api.gbif.org/v1/species/match?name='+ sys.argv[1])and execute it with, say,
$python myrequest.py Tracheophytain the terminal.Example: open-meteo
Another example of a useful API for weather data is https://open-meteo.com/en/docs#api_documentation. You can find a customized
requestspackage for open-meteo at https://pypi.org/project/openmeteo-requests/.import openmeteo_requests openmeteo = openmeteo_requests.Client() # Make sure all required weather variables are listed here # The order of variables in hourly or daily is important to assign them correctly below url = "https://api.open-meteo.com/v1/forecast" params = { "latitude": [38.74,41.15], # Lisbon, Porto "longitude": [-9.14,-8.6] , "current": ["temperature_2m", "relative_humidity_2m","wind_speed_10m"], } responses = openmeteo.weather_api(url, params=params) # Process first location. Add a for-loop for multiple locations or weather models for i in range(len(responses)): response = responses[i] print(f"Coordinates: {response.Latitude()}°N {response.Longitude()}°E") print(f"Elevation: {response.Elevation()} m") # Process current data. The order of variables needs to be the same as requested. current = response.Current() print(f"Current temperature_2m: {current.Variables(0).Value()}") print(f"Current relative_humidity_2m: {current.Variables(1).Value()}") print(f"Current wind_spreed_10m: {current.Variables(2).Value()}") print()
- Suggested problem
Solve problem *Bitcoin price index* from CS50P [Problem_set_4](https://cs50.harvard.edu/python/2022/psets/4/). In particular, organize your code so the main function is the following: ``` def main(): x=read_command_line_input() price=get_bitcoin_price() print(f"${x*price:,.4f}") ```---
Class 6 (October 16, 2026): virtual environments; file I/O with images
- For virtual environments, watch How to Use Virtual Environments with the Built-In venv Module (17’) and see the official documentation https://docs.python.org/3/library/venv.html
- For file I/O and images with Pillow, see lecture https://cs50.harvard.edu/python/weeks/6/ (sections on
open,withand Binary Files and PIL). CSV files will be discussed later with Pandas.In this class we build a small project,
image_project, that downloads and processes images of leaves. First we prepare a virtual environment with the packages the project needs (requestsandPillow), then we use it to read and write text and image files. At the end of the class, the project folder should look like this:image_project/ <- all commands are run from here |--- .venv/ <- the virtual environment (created by python -m venv .venv; not shared) |--- .gitignore <- tells Git to ignore .venv/ |--- requirements.txt <- list of packages and versions (shared) |--- images/ <- input images |--- output/ <- files created by our scripts |--- notes.py |--- download_image.py |--- explore_image.py |--- process_images.py
1. Why virtual environments?
Suppose your script was written and tested with version 12.0.0 of Pillow, but a colleague has an older version installed that behaves differently, or another project of yours needs a different version. Installing every package in the same place (the base environment) quickly leads to conflicts. A virtual environment solves this problem. It is:
- A folder that contains a Python interpreter and a set of packages, with given versions, for one project. It is isolated from other virtual environments and from the packages installed in the base environment.
- Conventionally named
.venvand placed at the root of the project folder. Tools such as VS Code detect it automatically.- Disposable: it should be simple to delete and re-create it from scratch. You never place your scripts or data inside
.venv.- Not shared (e.g. not checked into Git) and not copied to another computer. Instead, one shares the list of packages (
requirements.txt) and each person re-creates the environment.Let’s first look at the base environment. The following commands list the installed packages and show information about the package
requests(version, location, dependencies):$ pip list $ pip show requestsThe commands below work in the CS50 codespace, which runs Linux. When a command is different in Windows, the Windows version is given in a comment.
2. Create and activate a virtual environment
- Create the project folder and move into it. From now on, all commands are executed from
image_project:$ mkdir image_project $ cd image_project- Create the virtual environment. This creates the folder
.venvinsideimage_project:image_project/ $ python -m venv .venvSince the name starts with a dot,
.venvis a hidden folder: usels -ato see it.- Activate the environment:
image_project/ $ source .venv/bin/activate # Windows: .venv\Scripts\activateThe prompt now starts with
(.venv), which shows that the environment is active: the commandspythonandpipnow refer to the Python and packages in.venv. To de-activate it, the command isdeactivate.- Check which Python is running and which packages are installed. A new environment has (almost) no packages:
(.venv) image_project/ $ python -c "import sys; print(sys.prefix)" (.venv) image_project/ $ pip list
sys.prefixis the folder of the Python installation in use: it should be the.venvfolder of your project.3. Install packages and create requirements.txt
- With the environment active, install the packages for the project, specifying the versions:
(.venv) image_project/ $ pip install pillow==12.0.0 requests==2.32.5Packages that these depend on (e.g.
urllib3forrequests) are installed automatically. Compare the result ofpip listandpip show requestswith what you got in the base environment: the version and the location are different.- Check where Python looks for packages:
(.venv) image_project/ $ python -c "import sys; print(sys.path)"The
site-packagesfolder inside.venvis listed, but the folder where the base environment packages are stored is not.- Save the list of installed packages and versions to
requirements.txt, at the root of the project:(.venv) image_project/ $ pip freeze > requirements.txtOpen
requirements.txtto see its contents: one line per package, such aspillow==12.0.0.- Tell Git to ignore the
.venvfolder, by creating a file.gitignorewith the line.venv/:(.venv) image_project/ $ echo ".venv/" > .gitignoreIf you share the project (e.g. on GitHub), you share the scripts,
requirements.txtand.gitignore, but not.venv.4. Delete and re-create the environment
Since
.venvis disposable, we can delete it and re-create it fromrequirements.txt. This is also what a colleague does after downloading your project.(.venv) image_project/ $ deactivate image_project/ $ rm -rf .venv # Windows (PowerShell): Remove-Item -Recurse -Force .venv image_project/ $ python -m venv .venv image_project/ $ source .venv/bin/activate # Windows: .venv\Scripts\activate (.venv) image_project/ $ pip install -r requirements.txt (.venv) image_project/ $ pip listExercise: install one more package in your environment (say,
emoji), updaterequirements.txt, and send the file to a colleague. Your colleague should create a new project folder with a.venvenvironment from yourrequirements.txtand check withpip listthat both environments have the same packages and versions.5. Reading and writing text files
First, create the folders for the input and output files:
(.venv) image_project/ $ mkdir images outputIn this step we write a script
notes.py, in theimage_projectfolder, that writes some notes about the leaf to a text file and then reads them back. Add each piece of code below at the end ofnotes.py, and run the script after each addition with(.venv) image_project/ $ python notes.pyThen check the contents of
output/notes.txt(open it in the editor, or usecat output/notes.txtin the terminal).
openis a function built into Python that allows you to open a file to read from it or to write to it. The second argument is the mode:"w"(write),"a"(append) or"r"(read, the default). In the code below, the file is opened in writing mode: if the file already exists, its previous contents are deleted. This is why runningnotes.pyseveral times does not accumulate notes: each run starts with an empty file.# notes.py file = open("output/notes.txt", "w") file.write("olive leaf, upper surface dark green\n") file.close()Notice that
writedoes not add a new line: we need to include"\n"at the end of the string. To add new contents at the end of an existing file, open it in mode"a"(append) instead.Instead of explicitly opening and closing a file, it’s simpler and safer to use a context manager, with the keyword
with, which automatically closes the file at the end of the block, even if an error occurs. Add the following lines tonotes.py, which append a second note to the file:with open("output/notes.txt", "a") as f: f.write("olive leaf, lower surface silvery\n")To read the file, open it in mode
"r". The methodreadlinesreturns a list where each element is a line of the file (including the"\n"at the end). Add these lines tonotes.py, so the script prints what it wrote:with open("output/notes.txt", "r") as f: lines = f.readlines() print(lines)It is also possible to read the file one line at a time, which is better for large files. Add this last block to
notes.pyand compare its output with the previous one:with open("output/notes.txt", "r") as f: for line in f: print(line.rstrip()) # rstrip() removes the "\n" at the end of the line6. Binary files: downloading and inspecting an image
Files can be text files (human-readable, like
notes.txtor a.pyscript) or binary files, like images. Binary files are opened with the modes"rb"and"wb", and their contents arebytesinstead ofstr.The following script
download_image.pydownloads a photo of an olive leaf (upper and lower surface) from Wikimedia Commons, using the packagerequests(see class 5). The image is inresponse.content, which is abytesobject, so the file is opened in mode"wb". Wikimedia Commons rejects requests without a descriptive User-Agent (the name of the program that makes the request), so we add one inheaders.# download_image.py import requests url = "https://upload.wikimedia.org/wikipedia/commons/b/b2/Olive-tree-leaf-0.jpg" headers = {"User-Agent": "greends-ipython-course/1.0"} response = requests.get(url, headers=headers) response.raise_for_status() # raises an exception if the download failed print(response.headers["Content-Type"], len(response.content), "bytes") with open("images/olive_leaf.jpg", "wb") as f: f.write(response.content)If the download fails, you can copy the same image from datasets/olive_leaf.jpg in this repository. (Photo: Rnbc at en.wikipedia, CC BY-SA 3.0, via Wikimedia Commons.)
The first bytes of a file usually identify its format. Let’s read the first 4 bytes of the image:
with open("images/olive_leaf.jpg", "rb") as f: first_bytes = f.read(4) print(first_bytes) # b'\xff\xd8\xff\xe0'All JPEG files start with
b'\xff\xd8', and all PNG files start withb'\x89PNG'. Try to open the image in text mode ("r") and see what happens.7. Images with Pillow
Reading the bytes of an image is not enough to work with its pixels: we need to decode the file format. The package
Pillow(imported asPIL) does that for many formats. The following scriptexplore_image.pyopens the image, prints its properties and saves three new images inoutput.# explore_image.py from PIL import Image with Image.open("images/olive_leaf.jpg") as img: print(img.format, img.size, img.mode) # JPEG (1024, 768) RGB # each pixel of an RGB image is a tuple (red, green, blue) with values between 0 and 255 print("upper surface:", img.getpixel((450, 220))) # (x, y) = (column, row), from the top-left corner print("lower surface:", img.getpixel((500, 580))) gray = img.convert("L") # "L" means grayscale: one value between 0 (black) and 255 (white) per pixel gray.save("output/olive_leaf_gray.png") # the format is given by the extension thumb = img.copy() thumb.thumbnail((256, 256)) # resizes in place, keeping the proportions thumb.save("output/olive_leaf_thumb.jpg") img.rotate(90, expand=True).save("output/olive_leaf_rotated.jpg")Questions:
- Which surface of the leaf is brighter? Compare the values of
gray.getpixel(...)at the same two points.- What is the size of the thumbnail? Why is it not
(256, 256)?- Check the first bytes of
output/olive_leaf_gray.pngas in step 6: is it a PNG file?8. Exercise: process a folder of images
Write a script
process_images.pythat creates a grayscale thumbnail of each image in a folder, and keeps a log of the processed files. The input and output folders are given in the command line (seesys.argvin class 5):(.venv) image_project/ $ python process_images.py images outputFor each image
name.jpg(or.jpeg,.png) inimages, the script savesoutput/name_thumb.pngand appends a linename.jpg -> output/name_thumb.pngto the fileoutput/log.txt. Other files in the folder are ignored. Add a few more leaf images toimagesto test it.Use the template below. The module
os(standard library) providesos.listdir(folder), which returns the list of file names in a folder,os.path.join(folder, name), which builds a path, andos.path.splitext(name), which splits a name such as"olive_leaf.jpg"into("olive_leaf", ".jpg").import os import sys from PIL import Image def main(): input_folder, output_folder = read_command_line() for file_name in os.listdir(input_folder): if is_image(file_name): input_path = os.path.join(input_folder, file_name) output_path = make_thumbnail(input_path, output_folder) write_log(output_folder, file_name, output_path) def read_command_line(): '''returns the input and output folders; exits with a usage message if they are missing''' ... def is_image(file_name): '''returns True if the extension of file_name is .jpg, .jpeg or .png (in lower or upper case)''' ... def make_thumbnail(input_path, output_folder, size=(256, 256)): '''saves a grayscale thumbnail of the image as a png file in output_folder and returns its path''' name = os.path.splitext(os.path.basename(input_path))[0] output_path = os.path.join(output_folder, name + "_thumb.png") ... return output_path def write_log(output_folder, file_name, output_path): '''appends the line "file_name -> output_path" to output_folder/log.txt''' ... main()Suggested problem: CS50 P-Shirt from CS50P Problem set 6.
---
Class 7 (October 23, 2026): Jupyter notebooks; tabular data with pandas
- Getting started with pandas: https://pandas.pydata.org/docs/getting_started/intro_tutorials/index.html
- Videos: overview by Rob Mulla, Learning Pandas for Data Analysis? Start Here. (22’); course by Corey Schafer, Pandas tutorial (11 videos)
- Tidy data: H. Wickham (2014), Tidy Data, Journal of Statistical Software 59(10)
The goal of this class is not to memorise pandas syntax, which is well documented, but to understand how pandas thinks about data, because that is what makes it a tool for data science. All the code is in the notebook class7/class7_pandas.ipynb:
- Part 1 (expository): typed columns and vectorisation, boolean masks, the index.
- Part 2 (guided):
groupby,merge, tidy vs wide tables.The exercises on rural fires in Portugal (INE), which were Part 3 of this class, are now in Class 8.
Setup: a Jupyter notebook in the CS50 codespace
A Jupyter notebook (
.ipynb) mixes text cells (Markdown) and code cells. Code cells are run one at a time with Shift+Enter, and their results (tables, numbers, plots) appear right below. Variables stay in memory between cells, which makes notebooks convenient to explore data step by step.In the terminal of the CS50 codespace:
- Create a folder for the class and move into it:
$ mkdir class7 $ cd class7- Install
pandas, andipykernel, which allows VS Code to run notebooks. Here we install them in the default Python of the codespace, without a virtual environment: the notebook’s kernel picker in cs50.dev does not easily find a.venvcreated inside a subfolder. If a virtual environment is active (its name appears in the prompt), rundeactivatefirst.$ pip install pandas ipykernel- Download the notebook and open it:
$ wget https://raw.githubusercontent.com/isa-ulisboa/greends-ipython/main/class7/class7_pandas.ipynb $ code class7_pandas.ipynb- Click Select Kernel (top right of the notebook) → Python Environments → choose the default Python 3 (not a
.venv). If VS Code asks to install the Jupyter extension, accept.- Run the first cell. If it shows the pandas version, you are ready.
The datasets are read directly from the course repository, so there is no need to download them.
If the notebook cannot be set up in the codespace, open it in Google Colab (File → Open notebook → GitHub, then paste the notebook’s URL).
Part 1 — How pandas thinks
1.1 From a list of rows to a table of columns. Until now, we stored records as lists of dictionaries, one per observation. A
DataFramestores them as columns, and each column (aSeries) has a single data type (df.dtypes). ADataFramecan be thought of as a dictionary of columns that share the same row labels.1.2 Vectorisation: describe what, not how. Because a column has one type, an operation can be applied to the whole column at once, instead of looping over its elements:
# the "Python lists" way temp_f = [] for t in fires['temp']: temp_f.append(t * 9 / 5 + 32) # the pandas way fires['temp_f'] = fires['temp'] * 9 / 5 + 32The second version is shorter, and also much faster on large tables since the loop runs in compiled code.
1.3 The forest fires dataset. The dataset datasets/forestfires.csv describes 517 fires in the Montesinho natural park, in Portugal (source). The variables are:
X,Y: cell in a 9×9 grid over the park mapmonth,day: month (jantodec) and weekday (montosun); note that there is no yearFFMC,DMC,DC,ISI: indices of the Canadian Fire Weather Index (FWI) systemtemp: temperature (°C);RH: relative humidity (%);wind: wind speed (km/h);rain: rain (mm/m²)area: burned area (ha), from 0 to 1090.84The file is read with
pd.read_csvand explored with.head(),.shape,.info()and.describe(). Look before you compute: the mean and the median ofareaare very different, because most fires burned (almost) nothing.1.4 Boolean masks: asking questions to the data.
fires['temp'] > 25is a column ofTrue/False(a mask), andfires[mask]keeps the rows where the mask isTrue. Masks are combined with&(and),|(or),~(not), with each condition between parentheses, and.isin()checks membership in a list. SinceTruecounts as 1,mask.sum()counts andmask.mean()gives a proportion.
- Exercise 1.4: How many fires occurred on weekends? Is it more than expected if fires were spread evenly over the week?
1.5 The index: labels, not positions. Every row of a
DataFramehas a label (the index, shown on the left) and a position (0, 1, 2, …). Columns also have labels (the column names) and positions. There are two ways to select rows and columns:
.loc[rows, columns].iloc[rows, columns]selects by label position slice a:bincludes bexcludes b, like Python listsslice example fires.loc[0:4, 'temp':'wind']
5 rows (labels 0 to 4), columnstemp,RH,windfires.iloc[0:4, 8:10]
4 rows (positions 0 to 3), columnstemp,RH(positions 8, 9)typical use fires.loc[fires['month'] == 'aug', ['temp', 'RH']]fires.iloc[:20, 4:8]When a CSV file is read, the index is
0, 1, 2, ..., so labels and positions coincide andlocandilocseem to do the same thing. The difference appears as soon as rows are filtered, because the original labels are kept:hot = fires[fires['temp'] > 25] hot.iloc[0] # first row of hot (by position) hot.loc[0] # KeyError: there is no longer a row with label 0Labels do not need to be integers.
set_indexturns a column into the index:months = pd.DataFrame({'mth': ['jan', 'feb', 'mar'], 'name': ['January', 'February', 'March']}) months = months.set_index('mth') months.loc['feb', 'name'] # 'February'Why does the index exist? Alignment. When two
Seriesare combined, pandas matches their values by label, not by position. Labels that exist in only one of them giveNaN(missing value), instead of a silent mismatch:a = pd.Series([1, 2, 3], index=['jan', 'feb', 'mar']) b = pd.Series([10, 20, 30], index=['mar', 'jan', 'feb']) a + b # jan 21, feb 32, mar 13For instance,
fires.loc[fires['area'] > 10, 'month'].value_counts() / fires['month'].value_counts()gives the proportion of large fires for each month, although the two counts have months in different orders.If needed, labels and positions can be converted into each other:
fires.columns.get_indexer(['FFMC', 'DMC'])gives the positions of the columns, to use withiloc;fires.index[0:20]gives the labels of the first 20 rows, to use withloc.
- Exercise 1.5: Use
locto select the FWI variables for the fires in August and September. Then useilocto take the first 5 rows of the result. Why does.loc[0:4]not give the same result?Part 2 — Split, apply, combine; tidy and wide data
2.1
groupby.fires.groupby('month')['area'].sum()splits the rows by month, appliessumto each group and combines the results in a new table. This replaces the “loop and accumulate in a dictionary” pattern that we used in Classes 2 and 3. Several summaries can be computed at once with.agg(...):by_month = fires.groupby('month').agg(n_fires=('area', 'size'), total_area=('area', 'sum'))After
groupby, the group key becomes the index of the result, sorted alphabetically (apr,aug,dec, …)..reindex(labels)builds a newSeries(orDataFrame) with exactly the given labels, in the given order: existing labels keep their values, new labels getNaN, and labels not in the list are dropped. The tablemonthsof section 1.5 (with all 12 months, in the notebook) has the months in calendar order as its index, somonths.indexis exactly the list of labels needed:by_month = by_month.reindex(months.index) # rows from jan to dec
.reset_index()turns the index back into an ordinary column (useful before exporting or merging).
- Exercise 2.1: Group by
dayinstead. On which weekday is the median burned area largest? And the number of fires?2.2 Joining tables:
merge.mergecombines two tables through a common key, like a database join. In the notebook, a tablemonthswith the full name and the season of each month is merged withfires, to compare the burned area by season.2.3 Tidy vs wide data. A table is tidy when (1) each variable is a column, (2) each observation is a row, and (3) each type of observational unit is a table (Wickham, 2014). The forest fires table is tidy: one row per fire, one column per variable.
Two methods convert between the two shapes, and they are (roughly) inverse operations:
pivot_tablegoes from tidy to wide (the values of one column become the row labels,index, the values of another column become the column names,columns, and each cell summarises a third column,values, with a function,aggfunc), andmeltgoes from wide to tidy (the column names become the values of a new column). “Roughly” becausepivot_tableaggregates: aftermeltwe get back the totals per month and weekday, not the individual fires.A wide table spreads a variable across several columns. For instance, the total burned area by month and weekday (
fill_value=0fills the combinations without any fire):wide = fires.pivot_table(index='month', columns='day', values='area', aggfunc='sum', fill_value=0)Here, the weekday is a variable, but each weekday became a column. Wide tables are good for people (easy to read and compare, common in reports and in official statistics). Tidy tables are good for computing: filtering,
groupby,mergeand plotting all expect one variable per column.meltconverts a wide table into a tidy (long) one:long = wide.reset_index().melt(id_vars='month', var_name='day', value_name='area')Rule of thumb: keep data tidy while working, and make it wide at the end, for a report.
Reference: exporting to file
Exporting is done with methods named
.to_..., and reading with functions namedpd.read_..., as listed in https://pandas.pydata.org/docs/user_guide/io.html. For instance:
- Export a dataframe as a CSV file with
df.to_csv('filename.csv', index=False). Useindex=Falseunless the index contains information (for instance, aftergroupby); otherwise, the labels0, 1, 2, ...are written as an extra column.- Export as an Excel spreadsheet with
df.to_excel('filename.xlsx', sheet_name='fires', index=False)(requirespip install openpyxl).- Read an Excel spreadsheet with
pd.read_excel('filename.xlsx', sheet_name='fires').---
Class 8 (November 6, 2026): pandas, continued: select, sort, group, create
- pandas user guide: indexing and selecting data, working with text data, group by: split-apply-combine
- Videos: course by Corey Schafer, Pandas tutorial, in particular the videos on filtering, sorting, and grouping and aggregating
This class continues Class 7, and focuses on the four operations that are needed in assignment #3:
question pandas Which rows? masks: comparisons, &\|~,.between,.isin, string methods.str...In which order? sort_values(one or more columns),nlargestWhat is the summary for each group? groupby+aggHow do I create a new variable? arithmetic on columns; applywith a function or alambdaAll the code is in the notebook class8/class8_pandas.ipynb:
- Part 1 (guided): the four operations, on the forest fires dataset of Class 7.
- Part 2 (on your own): cleaning and analysing official statistics on rural fires in Portugal (INE). This was Part 3 of Class 7: if you have already done some of the exercises, continue from where you stopped.
The notebook is set up as in Class 7 (see Setup in Class 7), in a folder
class8:$ mkdir class8 $ cd class8 $ wget https://raw.githubusercontent.com/isa-ulisboa/greends-ipython/main/class8/class8_pandas.ipynb $ code class8_pandas.ipynb
Part 1 — Select, sort, group, create
1.1 Selecting rows. A condition on a column gives a mask (a column of
True/False), andfires[mask]keeps the rows where the mask isTrue. Conditions are combined with&(and),|(or),~(not), and each condition needs its own parentheses. The Python wordsandandordo not work on whole columns (ValueError: The truth value of a Series is ambiguous).fires[(fires['temp'] > 25) & (fires['RH'] < 30)] fires[fires['temp'].between(20, 22)] # 20 <= temp <= 22 fires[fires['month'].isin(['jun', 'jul', 'aug', 'sep'])] fires[~fires['month'].isin(['jun', 'jul', 'aug', 'sep'])]The methods of Python strings are applied to a whole text column through
.str, for instance.str.upper(),.str.startswith('j'),.str.contains('Vila'),.str.replace('&', ''),.str.len()or.str[:2](first two characters).fires.loc[mask, ['month', 'area']]selects rows and columns at the same time, andmask.sum()counts the rows.
- Exercise 1.1: How many fires occurred in summer with wind above 5 km/h and no rain?
1.2 Sorting.
sort_valuesreturns a new, sorted table. With a list of columns, rows are sorted by the first column, then by the second one for rows with the same value in the first column, and so on;ascendingcan be a list too:fires.sort_values('area', ascending=False) fires.sort_values(['month', 'area'], ascending=[True, False]) # within each month, largest fires first fires.nlargest(5, 'area')
- Exercise 1.2: Sort the fires by
dayand then bytemp(highest first), and show the 3 hottest fires of each weekday with.groupby('day').head(3).1.3 Group and aggregate. The pattern is
table.groupby(KEY)[COLUMN].FUNCTION()..aggcomputes several summaries at once, and named aggregation (new_name=(column, function)) chooses the names of the result. Operations can be chained: select, then group, then sort.fires.groupby('month')['area'].agg(['size', 'sum', 'mean', 'max']) fires.groupby('month').agg(n_fires=('area', 'size'), mean_temp=('temp', 'mean'), total_area=('area', 'sum')) fires[fires['area'] > 0].groupby('month')['area'].median().sort_values(ascending=False) fires.groupby(['month', 'day']).size().reset_index(name='n_fires') # two keys
- Exercise 1.3: For each weekday, compute the number of fires, the mean temperature and the total burned area, sorted by total burned area.
1.4 New variables:
applyandlambda. When the new variable is a formula, use arithmetic on columns (fires['area'] / 100). When the rule needs anif/else, or a function of your own,.apply(f)calls the functionfon each value of the column. A lambda is a function without a name, written in one line:lambda a: expressionis the same asdef f(a): return expression. Withaxis=1, the function is applied to each row, and can use several columns.def size_class(a): if a == 0: return 'none' elif a < 10: return 'small' else: return 'large' fires['size_class'] = fires['area'].apply(size_class) # a function, without () fires['burned'] = fires['area'].apply(lambda a: 'yes' if a > 0 else 'no') # a lambda fires['season'] = fires['month'].apply(lambda m: season[m]) # season is a dictionary fires['hot_dry'] = fires.apply(lambda row: row['temp'] > 25 and row['RH'] < 30, axis=1) # each row fires.groupby('size_class')['temp'].mean() # group by the new variableRule of thumb: use arithmetic and masks when possible (they are much faster), and
applywhen you need a function of your own.
- Exercise 1.4: Create a column
wind_class(calm,moderate,strong) withapply, and compute the mean burned area for each class. Create a columnweekendwithapplyand alambda, and compare the proportion of fires that burned something on weekends and on weekdays.Part 2 — On your own: rural fires in Portugal (INE)
The file datasets/rural_fires.csv was exported from INE, the Portuguese national statistics institute, and contains the number of rural fires in 2023 by geographic unit and by cause of fire. The geographic units follow the NUTS classification (version 2024), with hierarchical codes:
code level examples PTcountry Portugal 1 character NUTS I 1Continente,2Açores,3Madeira2 characters NUTS II 11Norte,19Centro,1AGrande Lisboa,1BPenínsula de Setúbal,1CAlentejo,1DOeste e Vale do Tejo,15Algarve,20,303 characters NUTS III 111Alto Minho,112Cávado, …7 characters municipality 1111601Arcos de ValdevezThe code of a municipality starts with the codes of its NUTS II (2 characters) and NUTS III (3 characters) regions. As with most official statistics, the file was prepared for people and not for computing:
- the first 11 lines contain titles, and there are notes at the end of the file;
- the separator is
;;- each number is followed by ` &
, so pandas reads numbers as text (dtypeisobject, orstr` in pandas 3);- the first column combines a code and a name (
1111601: Arcos de Valdevez);- the rows mix different units (country, NUTS I, II, III regions, and municipalities);
- the table is wide: the cause of the fire is spread over six columns.
The notebook first shows the raw text of the file, and then reads only the data rows into a dataframe
rfwithpd.read_csv(..., sep=';', skiprows=11, nrows=347, header=None, usecols=range(8), names=[...]). The exercises follow a typical workflow: clean, then select, sort, create and group (Part 1), and finally report.Cleaning
- 2.1 Convert the numeric columns to integers, either with string methods on whole columns (
.str.replace,.str.strip,.astype(int)), or withapplyand alambdathat converts one value.- 2.2 Split the first column into
codeandnamewith.str.split(': ', n=1, expand=True).Selecting rows
- 2.3 Keep the municipalities (codes with 7 characters) in a dataframe
mun, and add columnsnuts2andnuts3with the first characters of the code.- 2.4 With masks: the number of fires in a list of cities (
.isin), in the municipalities whose name starts withVila, the municipalities without fires, and the municipalities of the Alentejo with more than 20 fires.Sorting
- 2.5 The 10 municipalities with the most fires, and the 3 municipalities with the most fires in each NUTS II region (sort by two columns).
Validating and creating new variables
- 2.6 Validate the data: does the sum over municipalities equal the total for Portugal? Is the total of each municipality equal to the sum of the six causes?
- 2.7 The percentage of fires due to negligence in each municipality.
- 2.8 With
apply: a class of municipality by number of fires (none,few,some,many), and the main cause of fire in each municipality (applyon rows withaxis=1).Grouping
- 2.9 For each NUTS II region: number of municipalities, total, mean and maximum number of fires; the region names with
merge; and the number of municipalities by region and main cause (two keys).Challenge
- 2.10 Convert the table into a tidy table with columns
code,name,nuts2,cause,n_fireswith.melt().- 2.11 Compute the percentage of each cause in each region, and present the result as a wide table (regions × causes).
- 2.12 Export the tidy table with
.to_csv('rural_fires_2023_tidy.csv', index=False)(see Reference: exporting to file in Class 7).
Class 9 (November 13, 2026): objects and classes; attributes and methods
- CS50P, Lecture 8: Object-Oriented Programming. For this class, the first part of the lecture (classes, attributes, methods,
raise,__str__) is enough.- CS50P. In particular, check the shorts on classes, methods and variables, in this order: Classes, Instance Variables, Instance Methods, and Class Methods and Class Variables.
The goal of this class is to understand what an object is, and to write your first classes. As an example, we model a plant nursery: plants grow in trays, all plants of a tray are of the same species, and plants can be added to or removed from a tray. We want to keep track of the trays and of the number of plants of each species.
- Part 1 (expository): you have been using objects for weeks.
- Part 2 (guided): from a dictionary to a class:
__init__,self, attributes.- Part 3 (guided): methods with rules,
__str__, objects that contain objects.- Part 4 (on your own): a nursery inventory.
Create a folder
class9in the CS50 codespace and write the code of Parts 2 and 3 in a script, for instancenursery.py, and run it withpython nursery.py. Short tests can also be done in the Python REPL (typepythonin the terminal).
Part 1 — You have been using objects
1.1 Data and actions. Every value in Python is an object, and every object belongs to a class (its type). An object bundles data (its attributes) and actions on that data (its methods):
s = "Quercus suber" type(s) # <class 'str'>: s is an object (an instance) of class str s.upper() # method: an action, with parentheses s.split() # another method of class str values = [12, 3, 7] type(values) # <class 'list'> values.append(5) # method: changes the list values.sort()In Class 7 we used both kinds with pandas:
fires.shapeandfires.columnsare attributes (data, no parentheses), whilefires.head()andfires.groupby('month')are methods (actions, with parentheses). The same happened with Pillow in Class 6 (img.sizeandimg.rotate(90)) and withrequestsin Class 5 (response.status_codeandresponse.json()).The function
dir(s)lists all attributes and methods of an object.
- Exercise 1.1: Create two strings
a = "olive"andb = "cork oak". Doa.upper()andb.upper()run the same code? Do they return the same result? Why?The answer to exercise 1.1 is the main idea of this class: all objects of a class share the same methods (the class is the blueprint), but each object has its own data. Today we write our own classes.
Part 2 — From a dictionary to a class
2.1 A tray as a dictionary. With what we know from Classes 2 and 3, a tray could be a dictionary, and actions on trays could be functions that receive the dictionary as their first argument:
def new_tray(species, n_plants): return {"species": species, "n_plants": n_plants} def add_plants(tray, n): tray["n_plants"] += n tray1 = new_tray("Pansy", 26) add_plants(tray1, 10) print(tray1["n_plants"])This works, but the data and the functions are separate: nothing says that
add_plantsis meant for trays, and any dictionary (or anything else) can be passed to it.2.2 The same tray as a class. A class puts the data and the functions together:
class Tray: def __init__(self, species, n_plants): self.species = species self.n_plants = n_plants def add_plants(self, n): self.n_plants += n tray1 = Tray("Pansy", 26) tray1.add_plants(10) print(tray1.n_plants)Compare the two versions line by line:
dictionary + functions class def new_tray(species, n_plants):def __init__(self, species, n_plants):tray1 = new_tray("Pansy", 26)tray1 = Tray("Pansy", 26)tray1["n_plants"]tray1.n_plants(an attribute)def add_plants(tray, n):def add_plants(self, n):(a method)add_plants(tray1, 10)tray1.add_plants(10)
Tray("Pansy", 26)creates a new, empty object of classTray, calls__init__to fill in its attributes, and returns the object. The object is called an instance of the class.selfis the object that the method works on, exactly like the argumenttrayin the dictionary version. When you writetray1.add_plants(10), Python runsTray.add_plants(tray1, 10): try both and check that they do the same thing.- By convention, class names use CamelCase (
Tray,PlantNursery), while variables, functions and methods use snake_case (n_plants,add_plants).2.3 Each instance has its own data.
tray1 = Tray("Pansy", 26) tray2 = Tray("Petunia", 28) tray2.add_plants(5) print(tray1.n_plants, tray2.n_plants)
- Exercise 2.3: Before running the code, predict what it prints. Then change
__init__so thatn_plantsis optional, with default value 0 (as for any function), and create a tray withTray("Begonia").2.4 The most common errors. Each line marked
# bugbelow is wrong. Run the code, read the error message, and fix it:class Tray: def __init__(self, species, n_plants): self.species = species self.n_plants = n_plants def add_plants(n): # bug 1 self.n_plants += n def remove_plants(self, n): n_plants -= n # bug 2 tray1 = Tray("Pansy", 26) tray1.add_plants(10) tray1.remove_plants(5) tray2 = Tray # bug 3 tray2.remove_plants(5)Part 3 — Methods with rules,
__str__, objects that contain objects3.1 Methods keep the object valid. A tray has room for at most 50 plants, and one cannot remove more plants than there are in the tray. If all changes to a tray go through its methods, the methods can enforce these rules, and raise an exception (Class 4) when a rule is broken:
class Tray: MAX_PLANTS = 50 # class attribute: shared by all trays def __init__(self, species, n_plants=0): if n_plants < 0 or n_plants > Tray.MAX_PLANTS: raise ValueError(f"n_plants must be between 0 and {Tray.MAX_PLANTS}") self.species = species self.n_plants = n_plants def add_plants(self, n): if self.n_plants + n > Tray.MAX_PLANTS: raise ValueError(f"there is only room for {Tray.MAX_PLANTS - self.n_plants} more plants") self.n_plants += n def remove_plants(self, n): if n > self.n_plants: raise ValueError(f"the tray only has {self.n_plants} plants") self.n_plants -= n try: tray = Tray('Pansy', n_plants=60) except ValueError as e: print(e) else: print(tray.n_plants) # runs only if the tray was created
MAX_PLANTSis a class attribute: it is defined once, in the class, and is the same for all trays (Tray.MAX_PLANTS).self.speciesandself.n_plantsare instance attributes: each tray has its own values.
- Exercise 3.1: Create a tray with 40 plants and try to add 20 plants. Then catch the error with
try/except ValueErrorand print the message of the exception.Note that Python does not prevent
tray1.n_plants = -5: methods only protect the object if we use them. We will see in Class 10 how to signal that an attribute should not be changed directly (encapsulation).3.2 Printing an object:
__str__. Tryprint(tray1): the result looks like<__main__.Tray object at 0x7f...>, which is not very informative. When an object is printed, or converted withstr(), or used in an f-string, Python calls its method__str__, which should return a string. Add this method to classTray:def __str__(self): return f"{self.n_plants} {self.species} plants"Methods whose names start and end with double underscores, like
__init__and__str__, are called special methods: we do not call them directly, Python calls them for us (Tray(...)calls__init__,print(...)calls__str__).
- Exercise 3.2: Change
__str__so that it also shows how many plants can still be added to the tray.3.3 Objects that contain objects. The nursery is also an object: it has a name and contains trays. Its attribute
traysis a dictionary whose values areTrayobjects, with the tray names as keys:class Inventory: def __init__(self, name): self.name = name self.trays = {} # tray name -> Tray object def add_tray(self, tray_name, tray): self.trays[tray_name] = tray def get_tray(self, tray_name): return self.trays[tray_name] def __str__(self): return f"{self.name}: {len(self.trays)} trays" nursery = Inventory("ISA nursery") nursery.add_tray("A1", Tray("Pansy", 25)) nursery.add_tray("A2", Tray("Petunia", 10)) nursery.get_tray("A2").add_plants(10) print(nursery) print(nursery.get_tray("A2"))
get_trayreturns the tray object itself, not a copy, sonursery.get_tray("A2").add_plants(10)changes the tray that is in the inventory.3.4 Puzzle: a shared dictionary. In this version, the trays can be given when the inventory is created, with an empty dictionary as the default value:
class Inventory: def __init__(self, name, trays={}): self.name = name self.trays = trays def add_tray(self, tray_name, tray): self.trays[tray_name] = tray north = Inventory("North") south = Inventory("South") north.add_tray("A1", Tray("Pansy", 25)) print(len(south.trays))
- Exercise 3.4: Predict what is printed, and run the code. Why?
The default value
{}is created only once, whendefis executed, and not each time the function is called: all inventories created without the argumenttraysshare the same dictionary. Rule: never use a list or a dictionary as a default value. Create it inside__init__(as in 3.3), or usetrays=Noneand thenif trays is None: trays = {}.3.5 Looking ahead. A class can be built from another class: for instance,
class MultipleUseTray(Tray):would inherit all attributes and methods ofTray, and only add or change what is different. This is called inheritance, and it is one of the main concepts of OOP that we will see in Class 10.Part 4 — On your own: a nursery inventory
Start from class
Tray(3.1, with__str__from 3.2) and classInventory(3.3) in the scriptnursery.py. Add the following methods to classInventory:
- 4.1
remove_tray(tray_name): removes a tray, and raisesValueErrorif there is no tray with that name.- 4.2
list_all_trays(): prints one line per tray, for instanceA1: 25 Pansy plants.- 4.3
n_plants(): returns the total number of plants in the nursery.- 4.4
plants_by_species(): returns a dictionary with the number of plants of each species, for instance{"Pansy": 57, "Petunia": 10}.- 4.5
empty_trays(): returns a list with the names of the empty trays.- 4.6
total_value(prices): returns the value of all plants in the nursery, wherepricesis a dictionary with the price of each species.
Some more challenging exercises:
- 4.7
sell(species, n): removesnplants of the given species, taking them from the trays in order (first empty the first tray of that species, then the next one, and so on). If there are not enough plants, raiseValueErrorwithout changing any tray.- 4.8
load_csv(path): adds trays read from a CSV file with columnstray,species,n_plants(create a small file yourself; see Class 6 for reading files).- 4.9
to_dataframe(): returns a pandasDataFramewith one row per tray and columnstray,species,n_plants(see Class 7). Use it to sort the trays by number of plants.