greends-ipython

Introduction to Python 2026/2027

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.


Online resources for the course

* 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
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 this and many other anecdotes about Python.

</blockquote>

</details> ---

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`. ```
---

Class 4 (October 2, 2026): handling exceptions in Python: catching and raising exceptions

  1. See CS50P lecture 3, in particular the short on handling exceptions: try, except, ValueError, KeyError.

  2. 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
  1. 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) ```
  1. Example from https://cs50.harvard.edu/python/2022/shorts/handling_exceptions/
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") ```
  1. 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) ```
  1. 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

  1. See lecture https://cs50.harvard.edu/python/weeks/4/

  2. 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.
Create and import your own module

Exercise: Create file named utils.py and file main.py in 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.py

The contents of utils.py are typically functions or constants that you can re-use in different contexts. Let’s suppose that utils.py has 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.py is the following file:

main.py (1st version)
import common.utils

def main():
    x=common.utils.get_integer()
    print(x)

main()

Run main.py from inside the class_5 folder (cd class_5, then python main.py), so that Python can find the common folder.

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.path as 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 utils
As explained on the recommended video, a python **package** is just a folder with modules and a special file named `__init__.py`
Pip install

Some modules, like sys, math or random, are part of the Python standard library: they come with Python and can be imported without installing anything. For instance, random provides 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 package requests (used below to access APIs) is installed with

$ pip install requests

and 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/

  1. sys.argv
Previously, 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.
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')
To run it, you can for instance execute the command `$python sum.py 1.2 4.3` in the terminal.
  1. 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 (key trackName)? Notice that &limit=2 in 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.py with

response=requests.get('https://api.gbif.org/v1/species/match?name='+ sys.argv[1])

and execute it with, say, $python myrequest.py Tracheophyta in 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 requests package 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()
  1. 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

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 (requests and Pillow), 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 .venv and 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 requests
    

    The 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
    1. Create the project folder and move into it. From now on, all commands are executed from image_project:
      $ mkdir image_project
      $ cd image_project
      
    2. Create the virtual environment. This creates the folder .venv inside image_project:
      image_project/ $ python -m venv .venv
      

      Since the name starts with a dot, .venv is a hidden folder: use ls -a to see it.

    3. Activate the environment:
      image_project/ $ source .venv/bin/activate      # Windows: .venv\Scripts\activate
      

      The prompt now starts with (.venv), which shows that the environment is active: the commands python and pip now refer to the Python and packages in .venv. To de-activate it, the command is deactivate.

    4. 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.prefix is the folder of the Python installation in use: it should be the .venv folder of your project.

    3. Install packages and create requirements.txt
    1. 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.5
      

      Packages that these depend on (e.g. urllib3 for requests) are installed automatically. Compare the result of pip list and pip show requests with what you got in the base environment: the version and the location are different.

    2. Check where Python looks for packages:
      (.venv) image_project/ $ python -c "import sys; print(sys.path)"
      

      The site-packages folder inside .venv is listed, but the folder where the base environment packages are stored is not.

    3. Save the list of installed packages and versions to requirements.txt, at the root of the project:
      (.venv) image_project/ $ pip freeze > requirements.txt
      

      Open requirements.txt to see its contents: one line per package, such as pillow==12.0.0.

    4. Tell Git to ignore the .venv folder, by creating a file .gitignore with the line .venv/:
      (.venv) image_project/ $ echo ".venv/" > .gitignore
      

      If you share the project (e.g. on GitHub), you share the scripts, requirements.txt and .gitignore, but not .venv.

    4. Delete and re-create the environment

    Since .venv is disposable, we can delete it and re-create it from requirements.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 list
    

    Exercise: install one more package in your environment (say, emoji), update requirements.txt, and send the file to a colleague. Your colleague should create a new project folder with a .venv environment from your requirements.txt and check with pip list that 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 output
    

    In this step we write a script notes.py, in the image_project folder, 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 of notes.py, and run the script after each addition with

    (.venv) image_project/ $ python notes.py
    

    Then check the contents of output/notes.txt (open it in the editor, or use cat output/notes.txt in the terminal).

    open is 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 running notes.py several 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 write does 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 to notes.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 method readlines returns a list where each element is a line of the file (including the "\n" at the end). Add these lines to notes.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.py and 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 line
    
    6. Binary files: downloading and inspecting an image

    Files can be text files (human-readable, like notes.txt or a .py script) or binary files, like images. Binary files are opened with the modes "rb" and "wb", and their contents are bytes instead of str.

    The following script download_image.py downloads a photo of an olive leaf (upper and lower surface) from Wikimedia Commons, using the package requests (see class 5). The image is in response.content, which is a bytes object, 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 in headers.

    # 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 with b'\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 as PIL) does that for many formats. The following script explore_image.py opens the image, prints its properties and saves three new images in output.

    # 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.png as in step 6: is it a PNG file?
    8. Exercise: process a folder of images

    Write a script process_images.py that 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 (see sys.argv in class 5):

    (.venv) image_project/ $ python process_images.py images output
    

    For each image name.jpg (or .jpeg, .png) in images, the script saves output/name_thumb.png and appends a line name.jpg -> output/name_thumb.png to the file output/log.txt. Other files in the folder are ignored. Add a few more leaf images to images to test it.

    Use the template below. The module os (standard library) provides os.listdir(folder), which returns the list of file names in a folder, os.path.join(folder, name), which builds a path, and os.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.