From ecdeb6011ca43b728964dfaf723cf71bcbc10f4b Mon Sep 17 00:00:00 2001 From: l Date: Wed, 9 Sep 2026 14:19:23 +0100 Subject: [PATCH 01/33] Remove the unnecessary sidebar text --- org-cyf/content/sdc/tools/sprints/5/prep/index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/org-cyf/content/sdc/tools/sprints/5/prep/index.md b/org-cyf/content/sdc/tools/sprints/5/prep/index.md index b5a36b380..a8ba8773f 100644 --- a/org-cyf/content/sdc/tools/sprints/5/prep/index.md +++ b/org-cyf/content/sdc/tools/sprints/5/prep/index.md @@ -1,6 +1,5 @@ +++ title = "Prep" -description = "Overview description of the prep work for the sprint" layout = "prep" menu_level = ["sprint"] weight = 1 From d1abcff4dafb7768b37917f48b79e86b8d139e15 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 9 Sep 2026 15:57:30 +0100 Subject: [PATCH 02/33] rework intro to types --- .../type-checking-with-mypy/index.md | 51 +++----- .../module/decomposition/why-types/index.md | 120 +++++------------- 2 files changed, 50 insertions(+), 121 deletions(-) diff --git a/common-content/en/module/decomposition/type-checking-with-mypy/index.md b/common-content/en/module/decomposition/type-checking-with-mypy/index.md index eccaa863b..34c964cba 100644 --- a/common-content/en/module/decomposition/type-checking-with-mypy/index.md +++ b/common-content/en/module/decomposition/type-checking-with-mypy/index.md @@ -12,6 +12,19 @@ objectives = [ render = "never" +++ +## Support for type checking + +Different languages have different levels of support for checking types. + +Some languages, like Java, C++, Rust, and Go, _require_ you to write what types you expect function parameters to have. + +Other languages, like JavaScript and Python, _don't require_ this but they have tools which _allow_ you to add this information by using a tool like mypy or JSDoc. + +Some very low level machine languages like assembly don't have any typing at all. + +Languages with optional type checking perform good checks when you add this type information. If you don't add type annotations in your code, they will perform fewer checks. Sometimes they will infer the correct types based on what you _have_ annotated. Other times they will just ignore code with no annotations and not give you errors about it even if it's wrong. + +## Trying out Mypy Mypy is a tool which enables type checking in Python code. {{}} @@ -19,42 +32,12 @@ Read the first sections of [The Comprehensive Guide to mypy](https://dev.to/tush {{}} {{}} -Do not run the following code. + +**Task 4**: + +Have a look at `04-addmypy.py` This code contains bugs related to types. They are bugs mypy can catch. Read this code to understand what it's trying to do. Add type annotations to the method parameters and return types of this code. Run the code through mypy, and fix all of the bugs that show up. When you're confident all of the type annotations are correct, and the bugs are fixed, run the code and check it works. - -```python -def open_account(balances, name, amount): - balances[name] = amount - -def sum_balances(accounts): - total = 0 - for name, pence in accounts.items(): - print(f"{name} had balance {pence}") - total += pence - return total - -def format_pence_as_string(total_pence): - if total_pence < 100: - return f"{total_pence}p" - pounds = int(total_pence / 100) - pence = total_pence % 100 - return f"£{pounds}.{pence:02d}" - -balances = { - "Sima": 700, - "Linn": 545, - "Georg": 831, -} - -open_account("Tobi", 9.13) -open_account("Olya", "£7.13") - -total_pence = sum_balances(balances) -total_string = format_pence_as_str(total_pence) - -print(f"The bank accounts total {total_string}") -``` {{}} diff --git a/common-content/en/module/decomposition/why-types/index.md b/common-content/en/module/decomposition/why-types/index.md index bc6ae7868..4f4d84153 100644 --- a/common-content/en/module/decomposition/why-types/index.md +++ b/common-content/en/module/decomposition/why-types/index.md @@ -12,92 +12,48 @@ objectives = [ render = "never" +++ -In real life, as well as programming, there are some impossible operations. Can you divide seven by yellow? Can you set fire to a sound? These don't make sense. The same is true in programming. - -Given these functions: - -```python -def half(value): - return value / 2 - -def double(value): - return value * 2 -def second(value): - return value[1] -``` +In real life, as well as programming, there are some impossible operations. Can you divide seven by yellow? Can you set fire to a sound? These don't make sense. The same is true in programming. -Consider these blocks of code: +We are going to look at some functions which you can find in the file `SDC-Tools/sprint-5` directory. -```python -print(half(22)) -print(half("hello")) -print(half("22")) -``` +{{}} +**Task 1** -Is `half("22")` hoping to return 11 (because the string should be converted to a number)? Or return 2 (because it's the first half of the string)? Or error, because it doesn't make sense? +Have a look now at `01-predict.py`. -What is `half("hello")` meant to do? It probably doesn't make sense. +Take a moment to make predictions about what function calls will and will not work. -```python -print(double(22)) -print(double("hello")) -print(double("22")) -``` +Then try running the file and see what happens. +{{}} -Does `double("hello")` make sense? If so, what do you expect it to return? +In that file, is `half("22")` hoping to return 11 (because the string should be converted to a number)? Or return 2 (because it's the first half of the string)? Or error, because it doesn't make sense? -```python -print(second(22)) -print(second(0x16)) -print(second("hello")) -print(second("22")) -``` +What if we tried to run `half("hello")`? Try to give part of a word, or error because it can'tbe split evenly in half? Does this input even make sense? +What if we did `double("hello")` instead? What do you expect it to return? How about `second(22)`? Should it treat 22 like a stringified version of the decimal representation of the number 22 and return 2? If so - `22` is the same as `0x16`. Should `second(0x16)` convert `0x16` to decimal before returning the second character? Or should it remember that the original number was input as hexadecimal and return `6`? ## Intent -The _intent_ of these functions is probably that `half` and `double` are expected to operate on numbers, and `second` is expected to operate on strings (and/or maybe lists). +The _intent_ of these functions is probably that `half` and `double` are expected to operate on numbers, and `second` is expected to operate on strings (and/or maybe lists). We don't know for sure what the author intended just by looking at the function names. But Python lets us write all of these things. Some of them, like `half("hello")` will error when they run, maybe breaking our program. Others, like `double("22")` will succeed but in surprising ways which may cause our program to give more subtly incorrect results later on. -{{}} -Predict what `double("22")` will do. Then run the code and check. Did it do what you expected? Why did it return the value it did? -{{}} +In such a simple program as in `01-predict.py`, it's easy for us to run the program manually and see the errors (if we add enough logging). But as programs get bigger, these things get harder to spot, especially if there are branches and code only executes sometimes. -In such a simple program as above, it's easy for us to run the program manually and see the errors (if we add enough logging). But as programs get bigger, these things get harder to spot. +{{}} +**Task 2** -This gets even harder when code is only sometimes executed. For instance, consider this NodeJS program: +Have a look now at `02-playcomputer.py`. -```js -import process from "node:process"; -import readline from "node:readline"; +Read through this file and predict what it does. -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, -}); +Leave a comment if you spot any errors. -rl.question("What URL should we fetch?\n> ", async (url) => { - const response = await fetch(url); - if (!response.ok) { - if (response.body.toLowerCase().includes("permission")) { - console.error("You didn't have permission to get that URL"); - } else { - console.error(`The request failed - body: ${response.body}`); - } - process.exit(1); - } - - const body = await response.json(); - // TODO: Do something with the response. - - rl.close(); -}); -``` +{{}} -There is a bug here. `response.body` is a `Promise` not a string. So if a user ever tries to fetch a URL which returns a non-200 status code, our program will crash: +How many errors did you find in your testing? There is one big bug here which doesn't always show. `response.body` is a _stream_ not a _string_. So if a user ever tries to fetch a URL which returns a non-200 status code, our program will crash: ```console % node fetch.js @@ -114,45 +70,35 @@ TypeError: response.body.toLowerCase is not a function Node.js v22.11.0 ``` -The code we wrote was wrong. It could never have been correct. After a `fetch`, `response.body.toLowerCase()` _never_ makes sense. Ideally we shouldn't have needed to wait until running the code (in fact, running exactly that line of code with exactly that data) to find this out. +How easy was it to spot this bug in your testing? + +The code in this file was wrong. It could never have been correct. After a `fetch`, `response.body.toLowerCase()` _never_ makes sense. Ideally we shouldn't have needed to wait until running the code, and using that exact input, to find this out. ## Types This is where types come in. -Imagine if we could run some analysis over our code that told us "You're calling `double` with a string, but `double` expects a number, you have a bug". Or told us "You're calling `response.body.toLowerCase()` but `response.body` is a `Promise` which doesn't have a method `toLowerCase`, you have a bug". - -Now we wouldn't need to keep running our program with lots of different inputs every time we change it. The type analysis could tell us "You have a bug here, you should fix it". Without having to run the program, and without having to think about different possible inputs. - -## Support for type checking +Imagine if we could analyse our code and find out "You're calling `double` with a string, but `double` expects a number, you have a bug". Or that "You're calling `response.body.toLowerCase()` but `response.body` is a `ReadableStream` which doesn't have a method `toLowerCase`, you have a bug". -Different languages have different levels of support for checking types. +We wouldn't need to keep executing our program with lots of different inputs every time we change it. The type analysis could tell us "You have a bug here, you should fix it". Without having to run the program, and without having to think about different possible inputs. -Some languages, like Java, C++, Rust, and Go, _require_ you to write what types you expect function parameters to have. - -Other languages, like JavaScript and Python, don't _require_ this but they have tools which _allow_ you to add this information by using a tool like mypy or JSDoc. - -Languages with optional type checking perform good checks when you do add this type information. If you don't add type annotations in all of your code, they will perform fewer checks. Sometimes they will infer the correct types based on what you _have_ annotated. Other times they will just ignore code with no annotations and not give you errors about it even if it's wrong. ## Limits of type checking Types can be really useful for detecting bugs. But there are limits to what kind of bugs type checking can detect. -Take this code: +{{}} +**Task 3**: -```python -def double(number): - return number * 3 +Look at file `03-fix.py`. -print(double(10)) -``` +Read the code and see if you can find any bugs. -This code has a bug. +Write down what the bug is, and how would you fix it? -{{}} -Read the above code and write down what the bug is. How would you fix it? +Are there multiple ways you could fix it? {{}} -Even though we're calling `double` with the correct type, something is wrong. Either the name of `double` is wrong (it should be called `triple`), or what it's doing is wrong (it should do `* 2` not `* 3`). +Type checking can't catch this type of bug - as long as you give it a number as input, it gives you a number as output. All of the types are correct. Not all bugs are type errors. But checking for type errors can get rid of a lot of them. + -Type checking can't catch this bug. All of the types are correct. Not all bugs are type errors. But checking for type errors can get rid of a lot of bugs. From 851472824e39ab1cf712499c9a5aea19d04f4470 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 16 Sep 2026 15:27:40 +0100 Subject: [PATCH 03/33] merge poonam's dataclass change --- .../module/decomposition/dataclasses/index.md | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/common-content/en/module/decomposition/dataclasses/index.md b/common-content/en/module/decomposition/dataclasses/index.md index 2df62c974..02d83e598 100644 --- a/common-content/en/module/decomposition/dataclasses/index.md +++ b/common-content/en/module/decomposition/dataclasses/index.md @@ -27,7 +27,7 @@ Equality is one: ideally two value objects are the same if their fields are the class Person: def __init__(self, name: str, age: int, preferred_operating_system: str): self.name = name - self.age = age + self.age = age self.preferred_operating_system = preferred_operating_system imran = Person("Imran", 22, "Ubuntu") @@ -54,16 +54,17 @@ Python has a useful {{}}A decorator from dataclasses import dataclass @dataclass(frozen=True) -class Person: +class Animal: name: str + species: str age: int - preferred_operating_system: str + noise: str -imran = Person("Imran", 22, "Ubuntu") # We can call this constructor - @dataclass generated it for us. -print(imran) # Prints Person(name='Imran', age=22, preferred_operating_system='Ubuntu') +indigo = Animal("indigo", "cat", 2, "meow") # We can call this constructor - @dataclass generated it for us. +print(indigo) # Prints Animal(name='Indigo', species='cat', age=2, noise='meow') -imran2 = Person("Imran", 22, "Ubuntu") -print(imran == imran2) # Prints True +indigo2 = Animal("indigo", "cat", 2, "meow") +print(indigo == indigo2) # Prints True ``` The `dataclass` decorator generated a constructor, a `__str__` method (which is called when string formatting the value), and a custom `__eq__` method (which is called when comparing two values). This saves us having to write all of that code. @@ -71,7 +72,9 @@ The `dataclass` decorator generated a constructor, a `__str__` method (which is Other languages have a similar idea of a value type, and tools to help make them, such as [Java's record classes](https://docs.oracle.com/en/java/javase/17/language/records.html) and [C#'s' structure types](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct). {{}} -Write a `Person` class using `@datatype` which uses a `datetime.date` for date of birth, rather than an `int` for age. +Convert your existing `Person` class into a value type using `@datatype` so you can print the class (and see it's type and fields) and compare class instances that are identical. Make sure your `is_adult` method and `drivers_license_check` free function both work as normal. + +Make a new method on your Person class - `greet` which should return `"Hello !"` when used. -Re-add the `is_adult` method to it. +Take a look at the [`@datatype` documentation](https://docs.python.org/3/library/dataclasses.html) - what does `frozen=True` do to the class? What other options could you play around with and explore? {{}} From a4cead8968941c5a32a328aaf75c7b1c9a6b0e32 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 16 Sep 2026 16:02:18 +0100 Subject: [PATCH 04/33] update classes --- .../classes-and-objects/index.md | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/common-content/en/module/decomposition/classes-and-objects/index.md b/common-content/en/module/decomposition/classes-and-objects/index.md index b2a2db903..1eb569dad 100644 --- a/common-content/en/module/decomposition/classes-and-objects/index.md +++ b/common-content/en/module/decomposition/classes-and-objects/index.md @@ -4,7 +4,7 @@ time = 30 objectives = [ "Describe the purpose of a class.", "Explain the relationship between a class and instances of that class.", - "Use classes in mypy.", + "Use classes in mypy and python.", ] [build] @@ -31,22 +31,22 @@ eliza = { This allows us to pass around the values of `imran` or `eliza`, and access all of the related information while we do. -We've also already seen that it is useful to know that you can't call `.lower()` on the value `2`. +We now know that typing can tell us if we make errors like calling `.lower()` on the numeric value `2`. -It would be useful for a type checker to tell us if we try to access a property of an object that that object doesn't have: +It would be useful for a type checker to tell us if we try to access a property of an object that that object doesn't have. Can mypy help us here? -```python -imran = { - "name": "Imran", - "age": 22, - "preferred_operating_system": "Ubuntu", -} +{{}} +**Task 5**: -print(imran["name"]) -print(imran["address"]) -``` +Have a look at `05-explain.py` + +This code contains some untyped objects. -This code doesn't work, but mypy can't tell us this. As far as it is concerned, a dictionary is a dictionary - it could contain any keys! +Try checking it with mypy before running the code and predict what you think will happen when you run the code. +{{}} + +This code doesn't work, but mypy can't tell us this. Remember how we said that type checking has its limits? +As far as mypy is concerned, a dictionary is a dictionary - it could contain any keys! Instead, we can use a {{}}A class is a template for an object. It lets us say what properties (and methods) all instances of that class will contain.{{}}. @@ -78,11 +78,13 @@ This code is saying: "There's a category of object called Person. Every instance The method called `__init__` is called a constructor - it is what is called when we construct a new instance of the class. -{{}} -Save the above code to a file, and run it through mypy. -Read the error, and make sure you understand what it's telling you. -{{}} +{{}} + You can use the names of classes in type annotations just like you can use types like `str` or `int`: @@ -94,9 +96,14 @@ print(is_adult(imran)) ``` {{}} -Add the `is_adult` code to the file you saved earlier. -Run it through mypy - notice that no errors are reported - mypy understands that `Person` has a property named `age` so is happy with the function. +**Task 6** +Have a look at file `06-classes.py`. + +Run mypy and fix any errors. + +Add a new function called `likes_apple` which takes a person as parameter and returns true only if the preferred operating system is either `iOS` or `macOS`. Add all the appropriate type annotations and make sure mypy has no errors. + +Compare objects and classes and explain some advantages and disadvantages of each. -Write a new function in the file that accepts a `Person` as a parameter and tries to access a property that doesn't exist. Run it through mypy and check that it does report an error. {{}} From 6c24cf1cd0fda5b2f41cffac36e04c8565cff1ed Mon Sep 17 00:00:00 2001 From: l Date: Wed, 16 Sep 2026 16:53:38 +0100 Subject: [PATCH 05/33] update methods --- .../classes-and-objects/index.md | 2 +- .../en/module/decomposition/methods/index.md | 49 ++++++++++++++++--- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/common-content/en/module/decomposition/classes-and-objects/index.md b/common-content/en/module/decomposition/classes-and-objects/index.md index 1eb569dad..262f157ba 100644 --- a/common-content/en/module/decomposition/classes-and-objects/index.md +++ b/common-content/en/module/decomposition/classes-and-objects/index.md @@ -83,7 +83,7 @@ The method called `__init__` is called a constructor - it is what is called when question="What of the following best describes an 'instance' of a class?" answers="The variables that are accessed using self, like `self.name` | A class with attributes set to values passed into the constructor | The __init__ function that takes some values as arguments | A description of what a class contains" feedback=" No, these are called class attributes | Yes, an instance is one specific copy of a class | __init__ is the constructor of a class in python | No, a class already is a description of what it contains. An instance is more specific." - correct="2" >}} + correct="1" >}} You can use the names of classes in type annotations just like you can use types like `str` or `int`: diff --git a/common-content/en/module/decomposition/methods/index.md b/common-content/en/module/decomposition/methods/index.md index dff21489e..9aa2bc55d 100644 --- a/common-content/en/module/decomposition/methods/index.md +++ b/common-content/en/module/decomposition/methods/index.md @@ -5,7 +5,8 @@ objectives = [ "Define a method.", "Define a free function.", "Explain why methods can be more useful than free functions.", - "Implement a method on a class.", + "Explain how encapsulation can benefit class design.", + "Amend a method on a class.", ] [build] @@ -38,25 +39,59 @@ class Person: return self.age >= 18 imran = Person("Imran", 22, "Ubuntu") -print(imran.is_adult()) +print(imran.is_adult()) # True ``` This has a few advantages over {{}}A free function is a function that isn't a method. It isn't bound to a particular type (but may take parameters).{{}}. {{}} -Think of the advantages of using methods instead of free functions. Write them down in your notebook. + +**Task 7** + +What is the difference between methods and free functions? + +Do some research and think of the advantages of using methods instead of free functions. + +Write your thoughts down in `07-methods.txt`
Expand for some answers after you've listed your own. -* Ease of documentation - it makes it easier to find all of the things related to a string (or a Person) if they're attached to that type. -* Encapsulation - if we change the implementation of `Person` (e.g. we start storing a date of birth instead of an age), it's more obvious what things we need to change. +- Encapsulation - if we change the implementation of `Person` (e.g. we start storing a date of birth instead of an age), it's more obvious what things we need to change. +- Ease of documentation - it makes it easier to find all of the things related to a string (or a Person) if they're attached to that type.
{{
}} +Consider this free function called `drivers_license_check` which uses the Person class method `is_adult` outside of the class: + +```python +def drivers_license_check(person: Person): + if person.is_adult() == True: + return 'Valid drivers license' + + return 'This person is underage!' + +print(drivers_license_check(imran)) # returns 'Valid drivers license' +``` + {{}} -Change the `Person` class to take a date of birth (using [the standard library's `datetime.date` class](https://docs.python.org/3/library/datetime.html#datetime.date)) and store it in a field instead of `age`. -Update the `is_adult` method to act the same as before. +**Task 8** + +Work inside the `08-implement.py` file for this task. + +1. Add the `drivers_license_check` free function and the `is_adult` method into your code, and make sure your code currently gives the expected output. +1. Change the `Person` class to take a date of birth (using [the standard library's `datetime.date` class](https://docs.python.org/3/library/datetime.html#datetime.date)) and store the `date of birth` in a field instead of `age` (it should be a `str`). Don't change anything else. +1. **Try to run your code**, how does this change break your code. What kind of error do you get? Is it helpful in identifying where your next change needs to be? +1. Update the `is_adult` method so the error is fixed. Using the `drivers_license_check` function check everything runs as expected, it should return "Valid drivers license". _You should not change `drivers_license_check`_. +{{}} + +{{}} +Take a moment to consider what we've done here. How has **encapsulation** helped us make changes to our class? + +We've changed a property of Person, seen errors inform us about how that change affected a method on the class, and then amended that method so we were maintaining the behaviour of the class. The behaviour of `drivers_license_check` did not need to change - we can change the internal implementation of the class without affecting external code. + +_Encapsulation is a widely known principle in object-oriented programming, consider reading around online to find out more_ + {{}} From d8bb2b51fbe41388a1793b08792ca351140aa824 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 16 Sep 2026 17:05:50 +0100 Subject: [PATCH 06/33] update dataclass task description --- common-content/en/module/decomposition/dataclasses/index.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common-content/en/module/decomposition/dataclasses/index.md b/common-content/en/module/decomposition/dataclasses/index.md index 02d83e598..28d8ea8b5 100644 --- a/common-content/en/module/decomposition/dataclasses/index.md +++ b/common-content/en/module/decomposition/dataclasses/index.md @@ -72,6 +72,11 @@ The `dataclass` decorator generated a constructor, a `__str__` method (which is Other languages have a similar idea of a value type, and tools to help make them, such as [Java's record classes](https://docs.oracle.com/en/java/javase/17/language/records.html) and [C#'s' structure types](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct). {{}} + +**Task 9** + +Work in file `09-implement.py` for this task. + Convert your existing `Person` class into a value type using `@datatype` so you can print the class (and see it's type and fields) and compare class instances that are identical. Make sure your `is_adult` method and `drivers_license_check` free function both work as normal. Make a new method on your Person class - `greet` which should return `"Hello !"` when used. From 777753f04740fc6dfdc04209e305f6e3780f88ca Mon Sep 17 00:00:00 2001 From: l Date: Wed, 23 Sep 2026 15:25:22 +0100 Subject: [PATCH 07/33] generics --- .../en/module/decomposition/generics/index.md | 173 +++++++++++++++--- 1 file changed, 144 insertions(+), 29 deletions(-) diff --git a/common-content/en/module/decomposition/generics/index.md b/common-content/en/module/decomposition/generics/index.md index 36476e8ba..b17b71748 100644 --- a/common-content/en/module/decomposition/generics/index.md +++ b/common-content/en/module/decomposition/generics/index.md @@ -13,6 +13,8 @@ objectives = [ render = "never" +++ +## A problem type checking can't spot + Sometimes we want to reason about more complicated type relationships than "this field is a string". Lists and dicts are examples of this. We may want to reason that every value in a list is a string. Consider this code: @@ -21,67 +23,180 @@ Consider this code: from dataclasses import dataclass @dataclass(frozen=True) -class Person: +class Animal: name: str - children: list - -fatma = Person(name="Fatma", children=[]) -aisha = Person(name="Aisha", children=[]) - -imran = Person(name="Imran", children=[fatma, aisha]) + species: str -def print_family_tree(person: Person) -> None: - print(person.name) - for child in person.children: - print(f"- {child.name} ({child.age})") +@dataclass(frozen=True) +class Person: + name: str + age: int -print_family_tree(imran) +@dataclass(frozen=True) +class FamilyTree: + parent: Person + members: list + +pet = Animal(name="Gromit", species="Dog") +fatma = Person(name="Fatma", age=4) +aisha = Person(name="Aisha", age=6) +imran = Person(name="Imran", age=30) + +family = FamilyTree(parent=imran, members=[fatma, aisha, pet]) + +def print_family_tree(family: FamilyTree): + print(family.parent.name) + for child in family.members: + print(f"{child.name} ({child.age} years old)") + +print_family_tree(family) ``` +{{}} +**Task 10** +Have a look at the above code, you can find a copy in `10-predict.py` + There is a bug in this code. Can you spot it? Run your code through mypy. Does mypy spot it? +Offer an explanation for what is happening. +{{}} + In some languages, like Java, C#, Rust, or Go, type information is _required_ - you can't write code without it. This means those languages can do more checks, and give better error messages. We call these {{}}A statically typed language is a language where every variable has a fixed type. It is an error to try to assign a value to a variable with a different type.{{}} In other languages, like Python and JavaScript, type information is _optional_. Because of this, tools that check types are sometimes less strict. If they don't know what type something has, they stop doing any checks. -That's what's happening here. `Person.children` is a `list`, but mypy doesn't know what type of thing is in the list. It doesn't even know that everything in the list has the same type = `["hello", 7, True]` is a legal list in Python. +That's what's happening here. `FamilyTree.members` is a `list`, but mypy doesn't know what type of thing is in the list. It doesn't even know that everything in the list has the same type = `["hello", 7, True]` is a legal list in Python. Many people would consider a pet to be a member of the family, so it seems correct, but due to the different types, this code breaks down and mypy can't spot the problem. + +## Using Generics We can use {{}}A list could store numbers, or strings. We use generic types to say which type a particular instance of a list stores. Even though we can have a list of strings, and a list of numbers, the code for finding the first element is the same. But knowing that a list _only_ contains strings is useful.{{}} to tell mypy what type of thing is in the list: -```python {linenos=table} +```python from dataclasses import dataclass from typing import List @dataclass(frozen=True) -class Person: +class Animal: name: str - children: List["Person"] - -fatma = Person(name="Fatma", children=[]) -aisha = Person(name="Aisha", children=[]) + species: str -imran = Person(name="Imran", children=[fatma, aisha]) - -def print_family_tree(person: Person) -> None: - print(person.name) - for child in person.children: - print(f"- {child.name} ({child.age})") +@dataclass(frozen=True) +class Person: + name: str + age: int -print_family_tree(imran) +@dataclass(frozen=True) +class FamilyTree: + parent: Person + members: List[Person] + +pet = Animal(name="Gromit", species="Dog") +fatma = Person(name="Fatma", age=4) +aisha = Person(name="Aisha", age=6) +imran = Person(name="Imran", age=30) + +family = FamilyTree(parent=imran, members=[fatma, aisha, pet]) + +def print_family_tree(family: FamilyTree): + print(family.parent.name) + for child in family.members: + print(f"{child.name} ({child.age} years old)") + +print_family_tree(family) ``` +Try updating the code with this change and see if mypy spots the problem + Run this code through mypy. -Now that we've told mypy `Person.children` is a list of type `Person` (line 7), it can identify that the `child` variable on line 16 is of type `Person`. Because of this, it can tell us that `child.age` on line 17 doesn't exist. +Now that we've told mypy `FamilyTree.members` is a list of type `Person`, it can identify that the `child` variable printed out must be of type `Person`. Because of this, it can tell us that `child.age` on doesn't exist when the pet is accidentally included in the list. + > [!NOTE] > -> Most generics don't need the types to be quoted. Normally you'd just write `List[Person]`. But inside a type definition itself (i.e. inside the `Person` class), the `Person` type doesn't exist yet, so we need to quote it. +> Most generics don't need the types to be quoted. For example, you can write `List[Person]`. +> But if you want to recursively reference a type within the class, before the class has been defined, we need to quote it for mypy to recognise it. +> So for example, if we wanted a family tree to go several levels deep, e.g. to include grandchildren, we would write it as `List["FamilyTree"]`. > > It's kind of annoying, but don't worry about it too much. + +## Writing our own classes that use generics + +The kind of relationship structure we created with families and members, a {{}}A list could store numbers, or strings. We use generic types to say which type a particular instance of a list stores. Even though we can have a list of strings, and a list of numbers, the code for finding the first element is the same. But knowing that a list _only_ contains strings is useful.{{}}, is common across many types of data, for example how species of animal are related to each other, or how a dictionary might store words. + +Thinking about keeping our code reusable, is there a way we could define such structures, and be able to force them to work with certain types, without needing to write a special class for each individual data type? Just like lists can takea generic to force them to be a certain type, we can write classes that accept generics. + +Look at the following code: + +```python +from dataclasses import dataclass +from typing import List + +@dataclass(frozen=True) +class Animal: + name: str + size: str + +@dataclass(frozen=True) +class Person: + name: str + age: int + +@dataclass(frozen=True) +class Tree[T]: + parent: T + children: List[T] + + def print_tree(self): + print(self.parent) + for child in self.children: + print(child) + +fatma = Person(name="Fatma", age=4) +aisha = Person(name="Aisha", age=6) +imran = Person(name="Imran", age=30) +family_tree = Tree[Person](parent=imran, children=[fatma, aisha]) + +cats = Animal(name="Cat", size="Small") +dogs = Animal(name="Dog", size="Medium") +mammals = Animal(name="Mammals", size="Variable") +species_tree = Tree[Animal](parent=mammals, children=[cats, dogs]) + +family_tree.print_tree() +species_tree.print_tree() +``` + +The Tree here has a special type annotation given by `T`. This is a generic, telling python that whatever type is given, every reference to `T` within the class becomes that type. + +Observe that we then create two different trees: a `Tree` and a `Tree`. In these trees, the parent and list of children must contain `Person` and `Animal` types respectively. + +It also means instead of having to create a new function torpint out every single tree type, we can create a single function - `Tree.print_tree()`. + {{}} -Fix the above code so that it works. You must not change the `print` on line 17 - we _do_ want to print the children's ages. (Feel free to invent the ages of Imran's children.) +**Task 11** +Experiment with mypy and make sure that the family tree only takes `Person` types and the species tree only takes `Animal` types. + +We are going to improve the printing in the above code, you can find a copy in `11-fix.py` + +Currently the `Tree.print_tree()` function doesn't look very pretty. + +Change the Animal and Person classes, using whichever approach you think is best, to allow the `Tree.print_tree()` method to display an output that looks like this: + +``` +Imran (30 years old) +- Fatma (4 years old) +- Aisha (6 years old)) +Mammals (Variable size) +- Cat (Small size) +- Dog (Medium size) +``` + +**Stretch task** + +Think of another type of data that can be organised into a tree. + +Add a new class for this, instantiate some variables, and have the existing `Tree` class print it out. {{}} From ef574933589c1bba97996800d21f8360cc03df7c Mon Sep 17 00:00:00 2001 From: l Date: Wed, 23 Sep 2026 15:42:27 +0100 Subject: [PATCH 08/33] refactoring --- .../type-guided-refactorings/index.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/common-content/en/module/decomposition/type-guided-refactorings/index.md b/common-content/en/module/decomposition/type-guided-refactorings/index.md index 51253f16e..be1548a1e 100644 --- a/common-content/en/module/decomposition/type-guided-refactorings/index.md +++ b/common-content/en/module/decomposition/type-guided-refactorings/index.md @@ -12,11 +12,11 @@ objectives = [ render = "never" +++ -Using classes and objects can help us to understand and maintain codebases, particularly as they grow. +Using classes and objects can help us to understand and maintain codebases, particularly as they grow. The process of taking some old code, and updating it in a maintainable way is called "refactoring". -We've already identified that using methods instead of free functions can help us to encapsulate information. If we change our class from storing age as an `int` to storing date of birth as a `datetime.date`, it's easier to know what we're likely to need to change. +We previously saw that using methods instead of free functions can help us to encapsulate information. But changing functions into methods, and modifying classes can be tricky, as it is easy to forget places where code needs to be updated during refactoring. -Type checking can also help us with this. If you have some code which accesses `imran.age`, and we remove the `age` field, we can run mypy: It can tell us "Here are all of the places you also need to change your code". +Type checking can help us with this. If you have some code which accesses `imran.age`, and we remove the `age` field, we can run mypy: It will tell us "Here are all of the places that reference `age` you also need to change your code". Take this file as an example. It is a program that works out what laptops could be allocated to what people based on their preferred operating system. @@ -68,17 +68,22 @@ for person in people: Let's imagine we want to change our code. We don't want to say "Every person has one preferred operating system" any more. We want to let people have a list of operating systems they prefer (in order). So we could say "Imran prefers Ubuntu most of all, and then Arch Linux, but will not use macOS". {{}} +**Task 12** +A copy of this file is present in `12-refactor.py`. + Try changing the type annotation of `Person.preferred_operating_system` from `str` to `List[str]`. Run mypy on the code. -It tells us different places that our code is now wrong, because we're passing values of the wrong type. +It tells us different places that our code is now wrong. Fix it to remov eany errors. -We probably also want to _rename_ our field - lists are plural. Rename the field to `preferred_operating_systems`. +Now we changed the types, we probably also want to _rename_ our fields to something appropriate. Run mypy again. -Fix all of the places that mypy tells you need changing. Make sure the program works as you'd expect. +Fix all of the places that mypy tells you need changing. + +Then, make sure the program works as you'd expect. {{}} -The bigger (and more complicated) our codebase is, the more useful it is that mypy tells us what code needs changing. This is even more useful when we start working with code we didn't write ourselves, or we wrote long ago. Instead of needing to read all of the code and search around to try to work out where we need to change an `age` to `date_of_birth`, or a `preferred_operating_system` to a `preferred_operating_systems` (and maybe change from an `==` check to an `in` check), mypy can just tell us "here are all of the places that are wrong". +The bigger (and more complicated) our codebase is, the more useful it is that mypy tells us what code needs changing. This is even more useful when we start working with code we didn't write ourselves, or we wrote long ago. Instead of needing to read all of the code and search around to try to work out where we need to change an `age` to `date_of_birth`, or how to access a single variable that has become a list of many, mypy can tell us "here are all of the places that are wrong". From 701e19955f38d905ecfc412412f5861e195ca595 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 23 Sep 2026 16:09:25 +0100 Subject: [PATCH 09/33] enums --- .../en/module/decomposition/enums/index.md | 71 +++++-------------- 1 file changed, 18 insertions(+), 53 deletions(-) diff --git a/common-content/en/module/decomposition/enums/index.md b/common-content/en/module/decomposition/enums/index.md index b6497186f..190af6cee 100644 --- a/common-content/en/module/decomposition/enums/index.md +++ b/common-content/en/module/decomposition/enums/index.md @@ -22,9 +22,9 @@ Some common problems with strings: * Normalised values - are `"Arch Linux"` and `"Arch"` the same? Should they be? * Typos - is `"Arc Linux"` meant to be `"Arch Linux"`? Or is it a separate operating system? -In fact, in the previous example, the laptop with id 3 was never put in anyone's preferred list, because its operating system was spelled `Ubuntu` not `ubuntu`. +Did you spot in the bug in task 12? The laptop with id 3 was never put in anyone's preferred list, because its operating system was spelled `Ubuntu` not `ubuntu`. -We can use enums to represent that one some values are allowed, and make sure we're always using the same ones. This is similar to how in HTML we can use an `` instead of an `` to restrict what a user can enter into a form. +We can use enums to represent that one some values are allowed, and make sure we're always using the same ones. This is similar to how in HTML we can use a `` to restrict what a user can enter into a form. In Python, we can define an enum as a new type. This is like `bool` - `bool` is a type which has two possible values (`True` and `False`). We can make enums that have any number of possible values, and we can choose the values' names. @@ -39,65 +39,30 @@ class OperatingSystem(Enum): This defines a new type called `OperatingSystem` which has three possible values - `MACOS`, `ARCH`, and `UBUNTU`. We can use this type in a type annotation to make sure that we're only passed one of these values. If someone makes a typo in one of these values, mypy will catch it and tell us that `UBUNT` or `macOS` or `NIX` doesn't exist. -```python -from dataclasses import dataclass -from enum import Enum -from typing import List - -class OperatingSystem(Enum): - MACOS = "macOS" - ARCH = "Arch Linux" - UBUNTU = "Ubuntu" - -@dataclass(frozen=True) -class Person: - name: str - age: int - preferred_operating_system: OperatingSystem +> [!NOTE] +> +> There are lots of ways different programming deal with the concept enums. +> Some, like JavaScript, have no built-in way to use enums. +> Python treats enums as a special kind of class mapping definitions to a value. +> Others, like Rust, have more advanced typing systems that can treat enumerations as standalone types. -@dataclass(frozen=True) -class Laptop: - id: int - manufacturer: str - model: str - screen_size_in_inches: float - operating_system: OperatingSystem - +We know that when we save data, transfer it across a network, or take user input, everything comes in as bytes. A typical pattern in software is to accept a string in the user input, and convert it to an enum before passing it into any other function. If the string wasn't a valid operating system we know about, we will reject it and give an error when we first accept it. All of our other functions can take an `OperatingSystem` as a parameter, and know that any value it's given _must_ be a valid operating system. This restricts where we need to worry about incorrect input - once we've checked that the string was correct one time, the rest of our code doesn't have to worry about incorrect strings. -def find_possible_laptops(laptops: List[Laptop], person: Person) -> List[Laptop]: - possible_laptops = [] - for laptop in laptops: - if laptop.operating_system == person.preferred_operating_system: - possible_laptops.append(laptop) - return possible_laptops +{{}} +**Task 13** +Look at file `13-implement.py` -people = [ - Person(name="Imran", age=22, preferred_operating_system=OperatingSystem.UBUNTU), - Person(name="Eliza", age=34, preferred_operating_system=OperatingSystem.ARCH), -] +It currently handles operating systems as strings. -laptops = [ - Laptop(id=1, manufacturer="Dell", model="XPS", screen_size_in_inches=13, operating_system=OperatingSystem.ARCH), - Laptop(id=2, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system=OperatingSystem.UBUNTU), - Laptop(id=3, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system=OperatingSystem.UBUNTU), - Laptop(id=4, manufacturer="Apple", model="macBook", screen_size_in_inches=13, operating_system=OperatingSystem.MACOS), -] +Refactor the code to use enums for operating systems. -for person in people: - possible_laptops = find_possible_laptops(laptops, person) - print(f"Possible laptops for {person.name}: {possible_laptops}") -``` +Check with mypy and test it to ensure the program still works correctly. -We know that when we save data, transfer it across a network, or take user input, everything comes in as bytes. A typical pattern in software is to accept a string in the user input, and convert it to an enum before passing it into any other function. If the string wasn't a valid operating system we know about, we will reject it and give an error when we first accept it. All of our other functions can take an `OperatingSystem` as a parameter, and know that any value it's given _must_ be a valid operating system. This restricts where we need to worry about incorrect input - once we've checked that the string was correct one time, the rest of our code doesn't have to worry about incorrect strings. +Replace the list of existing people with [the `input` function](https://docs.python.org/3/library/functions.html#input) to read a person's name, age, and preferred operating system. -{{}} -Write a program which: -1. Already has a list of `Laptop`s that a library has to lend out. -2. Accepts user input to create a new `Person` - it should use [the `input` function](https://docs.python.org/3/library/functions.html#input) to read a person's name, age, and preferred operating system. -3. Tells the user how many laptops the library has that have that operating system. -4. If there is an operating system that has more laptops available, tells the user that if they're willing to accept that operating system they're more likely to get a laptop. +Make sure your implementation has a good user experience, and properly validates the inputs, mapping an OS to one of the enum values. -You should convert the age and preferred operating system input from the user into more constrained types as quickly as possible, and should output errors to stderr and terminate the program with a non-zero exit code if the user input bad values. +If an operating system can't be matched at all, your script should handle it appropriately and not crash. {{}} From 13d87dc1e2c6ede97e97e161acdf2ab9317d2ed5 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 23 Sep 2026 16:38:18 +0100 Subject: [PATCH 10/33] inheritance --- .../module/decomposition/inheritance/index.md | 92 ++++++++++--------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/common-content/en/module/decomposition/inheritance/index.md b/common-content/en/module/decomposition/inheritance/index.md index 54c79baee..08c6e659e 100644 --- a/common-content/en/module/decomposition/inheritance/index.md +++ b/common-content/en/module/decomposition/inheritance/index.md @@ -14,7 +14,9 @@ objectives = [ render = "never" +++ -Classes can extend other classes to share most of their functionality but add or replace some of it. +In this prep we have seen how add methods to classes to encapsulate functionality. We have seen how to use generics to force classes to work with certain types. Keeping code reusability and maintainability in mind, what if we wanted to add a new class that did mostly the same as an existing class, but with some slight changes? + +Classes can _extend_ other classes to share most of their functionality but add or replace some of it. A class that carries over something from another class is called _inheritance_. Read the following code: @@ -93,55 +95,55 @@ print(unsorted_values.max_gap_between_values()) # This doesn't work - the super ``` We have two classes that behave the same. They both have a constructor, and four methods (`first`, `last`, `largest`, `length`). `SortedImmutableNumberList` also has an extra method: `max_gap_between_values` which `ImmutableNumberList` does not have. +The method implementations are different for the two classes. They have different trade-offs to consider. -The `largest` implementation is different for the two classes. They have different trade-offs. If we will frequently need to get the largest value from the list, `ImmutableNumberList` is going to be slower, because it looks through every element every time it needs to find the largest value. If instead we will frequently need to get the length of the list, `ImmutableNumberList` is going to be faster, because it does less work in the constructor. +{{}} -{{}} -Programmers used to use inheritance a lot. Over time, many people are preferring composition over inheritance. +**Task 14** -Have a read of [this article describing the differences between composition and inheritance](https://sheldonrcohen.medium.com/favoring-composition-over-inheritance-ff2ece6b7b4e) and [this article exploring when each makes sense](https://www.thoughtworks.com/en-gb/insights/blog/composition-vs-inheritance-how-choose). +A copy of this code is in file `14-analyse.py` + +Try using this code and make sure you understand how it works and what it does + +Answer the following questions, writing your answers in the file, before checking the answers. + +Q1: If you know in advance you need to frequently access the largest item of the list, which class will be more efficient and why? + +Q2: If you know in advance you will be initialising many of them repeatedly, which class will be more efficient and why? + +
+ +Expand for some answers after you've listed your own. +Q1: `SortedImmutableNumberList` sorts the numbers in advance, and the method implementation for largest item only needs to look at the final item of the sorted list. This means accessing it is faster. +Q2: `ImmutableNumberList` doesn't need to sort the numbers immediately on creation. If you only intended to use `first` and `last`, it may be faster. + +Of course, it all depends on which functions you think you will need. +You will learn more about these efficiency concepts in the upcoming complexity module. + +
{{
}} +Many programming libraries will have different versions of classes optimised for different tasks, and even if the API to use them is the same, you should be careful considering which one is appropriate for your specific use case. +As you develop your own code, you may find it beneficial to extend certain classes to assist with certain tasks, and this may help you maintain your code or make it more efficient. +Inheritance is a great way of helping you achieve this. + + {{}} -Play computer with this code. Predict what you expect each line will do. Then run the code and check your predictions. (If any lines cause errors, you may need to comment them out to check later lines). +**Task 15** -```python -class Parent: - def __init__(self, first_name: str, last_name: str): - self.first_name = first_name - self.last_name = last_name - - def get_name(self) -> str: - return f"{self.first_name} {self.last_name}" - - -class Child(Parent): - def __init__(self, first_name: str, last_name: str): - super().__init__(first_name, last_name) - self.previous_last_names = [] - - def change_last_name(self, last_name) -> None: - self.previous_last_names.append(self.last_name) - self.last_name = last_name - - def get_full_name(self) -> str: - suffix = "" - if len(self.previous_last_names) > 0: - suffix = f" (née {self.previous_last_names[0]})" - return f"{self.first_name} {self.last_name}{suffix}" - -person1 = Child("Elizaveta", "Alekseeva") -print(person1.get_name()) -print(person1.get_full_name()) -person1.change_last_name("Tyurina") -print(person1.get_name()) -print(person1.get_full_name()) - -person2 = Parent("Elizaveta", "Alekseeva") -print(person2.get_name()) -print(person2.get_full_name()) -person2.change_last_name("Tyurina") -print(person2.get_name()) -print(person2.get_full_name()) -``` +Look at file `15-playcomputer.py` + +Play computer with this code + +Describe what is happening and why on each line that accesses the person objects + +If any lines cause errors, comment out the line and explain why the error happens +{{}} + + +{{}} +Inheritance is only one way of extending classes. +Another technique is called "composition" and this allows you to combine behaviours from many different classes. + +Have a read of [this article describing the differences between composition and inheritance](https://sheldonrcohen.medium.com/favoring-composition-over-inheritance-ff2ece6b7b4e) and [this article exploring when each makes sense](https://www.thoughtworks.com/en-gb/insights/blog/composition-vs-inheritance-how-choose). {{}} From 58f189cf1124e216b5e02b9fb061e9de4f60b131 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 30 Sep 2026 11:20:12 +0100 Subject: [PATCH 11/33] add encapsulation task --- .../module/decomposition/dataclasses/index.md | 2 +- .../en/module/decomposition/enums/index.md | 2 +- .../en/module/decomposition/generics/index.md | 4 +- .../module/decomposition/inheritance/index.md | 4 +- .../en/module/decomposition/methods/index.md | 70 +++++++++++++++++-- .../type-guided-refactorings/index.md | 2 +- 6 files changed, 72 insertions(+), 12 deletions(-) diff --git a/common-content/en/module/decomposition/dataclasses/index.md b/common-content/en/module/decomposition/dataclasses/index.md index 28d8ea8b5..3f09b471f 100644 --- a/common-content/en/module/decomposition/dataclasses/index.md +++ b/common-content/en/module/decomposition/dataclasses/index.md @@ -73,7 +73,7 @@ Other languages have a similar idea of a value type, and tools to help make them {{}} -**Task 9** +**Task 10** Work in file `09-implement.py` for this task. diff --git a/common-content/en/module/decomposition/enums/index.md b/common-content/en/module/decomposition/enums/index.md index 190af6cee..9bab1c011 100644 --- a/common-content/en/module/decomposition/enums/index.md +++ b/common-content/en/module/decomposition/enums/index.md @@ -50,7 +50,7 @@ This defines a new type called `OperatingSystem` which has three possible values We know that when we save data, transfer it across a network, or take user input, everything comes in as bytes. A typical pattern in software is to accept a string in the user input, and convert it to an enum before passing it into any other function. If the string wasn't a valid operating system we know about, we will reject it and give an error when we first accept it. All of our other functions can take an `OperatingSystem` as a parameter, and know that any value it's given _must_ be a valid operating system. This restricts where we need to worry about incorrect input - once we've checked that the string was correct one time, the rest of our code doesn't have to worry about incorrect strings. {{}} -**Task 13** +**Task 14** Look at file `13-implement.py` diff --git a/common-content/en/module/decomposition/generics/index.md b/common-content/en/module/decomposition/generics/index.md index b17b71748..861baf9ec 100644 --- a/common-content/en/module/decomposition/generics/index.md +++ b/common-content/en/module/decomposition/generics/index.md @@ -53,7 +53,7 @@ print_family_tree(family) ``` {{}} -**Task 10** +**Task 11** Have a look at the above code, you can find a copy in `10-predict.py` There is a bug in this code. Can you spot it? @@ -176,7 +176,7 @@ Observe that we then create two different trees: a `Tree` and a `Tree}} -**Task 11** +**Task 12** Experiment with mypy and make sure that the family tree only takes `Person` types and the species tree only takes `Animal` types. We are going to improve the printing in the above code, you can find a copy in `11-fix.py` diff --git a/common-content/en/module/decomposition/inheritance/index.md b/common-content/en/module/decomposition/inheritance/index.md index 08c6e659e..235155718 100644 --- a/common-content/en/module/decomposition/inheritance/index.md +++ b/common-content/en/module/decomposition/inheritance/index.md @@ -99,7 +99,7 @@ The method implementations are different for the two classes. They have differen {{}} -**Task 14** +**Task 15** A copy of this code is in file `14-analyse.py` @@ -129,7 +129,7 @@ Inheritance is a great way of helping you achieve this. {{}} -**Task 15** +**Task 16** Look at file `15-playcomputer.py` diff --git a/common-content/en/module/decomposition/methods/index.md b/common-content/en/module/decomposition/methods/index.md index 9aa2bc55d..2ae87ccef 100644 --- a/common-content/en/module/decomposition/methods/index.md +++ b/common-content/en/module/decomposition/methods/index.md @@ -5,8 +5,8 @@ objectives = [ "Define a method.", "Define a free function.", "Explain why methods can be more useful than free functions.", - "Explain how encapsulation can benefit class design.", "Amend a method on a class.", + "Explain how encapsulation can benefit class design.", ] [build] @@ -87,11 +87,71 @@ Work inside the `08-implement.py` file for this task. 1. Update the `is_adult` method so the error is fixed. Using the `drivers_license_check` function check everything runs as expected, it should return "Valid drivers license". _You should not change `drivers_license_check`_. {{}} -{{}} -Take a moment to consider what we've done here. How has **encapsulation** helped us make changes to our class? -We've changed a property of Person, seen errors inform us about how that change affected a method on the class, and then amended that method so we were maintaining the behaviour of the class. The behaviour of `drivers_license_check` did not need to change - we can change the internal implementation of the class without affecting external code. -_Encapsulation is a widely known principle in object-oriented programming, consider reading around online to find out more_ +## Encapsulation + +An advantage of classes over objects is encapsulation. + +Imagine you have written your Person class that stores age information. + +For privacy reasons, you don't want to reveal the exact age of the person, only whether they are or are not over 18 years old. + +This means we need to store the "age" value in a class, but somehow keep it _private_ to that class. The only _public_ information we want is whether or not they are over 18. How can we achieve this? + +Look at the following code: + +```python +class Person: + def __init__(self, name: str, age: int): + self.name = name + self.__age = age + + def is_adult(self): + return self.__age >= 18 + +imran = Person("Imran", 22) +print(imran.name) +# print(imran.age) # fails +# print(imran.__age) # fails +print(imran.is_adult()) # works and prints True + +eliza = Person("Eliza", 12) +print(eliza.name) +# print(eliza.age) # fails +# print(imran.__age) # fails +print(eliza.is_adult()) # works and prints False +``` + +In python, any class property that begins with two underscores is considered _private_, i.e. it can only be used within that specific class instance. + +> [!NOTE] +> +> Using underscores, Python doesn't have a clear way of marking something as private. +> Other programming languages like Java mark this more explicitly with keywords like "private" and "public". +> It's worth becoming familair with this private/public language even if you're not using it right now. +> + +You can now program classes to change behaviour based on the information stored within them. +Compare this with objects, which can only ever store data, and behave the same every time. + +Another benefit of encapsulation is letting you make "read only" properties. +Think about the example above. +Imagine you wanted to check if a `Person` class had a certain name using an equality test, but accidentally only used a single `=` symbol: +```python +imran.name = "Eliza" +``` +Python allows you to change properties whenever you want. +If `name` were private, and the only way to access it was through a `get_name()` method that returns a string, it would be impossible to accidentally change the value. +In this way, encapsulation can be used to prevent accidental errors in code. + +{{}} +**Task 9** + +Start by reading [Python encapsulation](https://www.w3schools.com/python/python_encapsulation.asp) and think about some of the benefits that encapsulation can add to a class. + +Do some further research of your own to learn about encapsulation. +Think of some examples and in your own words write down some benefits and trade-offs of using encapsulation in classes in the file `09-encapsulation.txt` {{}} + diff --git a/common-content/en/module/decomposition/type-guided-refactorings/index.md b/common-content/en/module/decomposition/type-guided-refactorings/index.md index be1548a1e..dbd6d5417 100644 --- a/common-content/en/module/decomposition/type-guided-refactorings/index.md +++ b/common-content/en/module/decomposition/type-guided-refactorings/index.md @@ -68,7 +68,7 @@ for person in people: Let's imagine we want to change our code. We don't want to say "Every person has one preferred operating system" any more. We want to let people have a list of operating systems they prefer (in order). So we could say "Imran prefers Ubuntu most of all, and then Arch Linux, but will not use macOS". {{}} -**Task 12** +**Task 13** A copy of this file is present in `12-refactor.py`. Try changing the type annotation of `Person.preferred_operating_system` from `str` to `List[str]`. From ea7472fbba5a587390931916087981b3799ae846 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 30 Sep 2026 11:25:50 +0100 Subject: [PATCH 12/33] add encapsulation stretch task --- common-content/en/module/decomposition/methods/index.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common-content/en/module/decomposition/methods/index.md b/common-content/en/module/decomposition/methods/index.md index 2ae87ccef..3bff309bf 100644 --- a/common-content/en/module/decomposition/methods/index.md +++ b/common-content/en/module/decomposition/methods/index.md @@ -152,6 +152,9 @@ Start by reading [Python encapsulation](https://www.w3schools.com/python/python_ Do some further research of your own to learn about encapsulation. -Think of some examples and in your own words write down some benefits and trade-offs of using encapsulation in classes in the file `09-encapsulation.txt` +Think of some examples and in your own words write down some benefits and trade-offs of using encapsulation in classes in the file `09-encapsulation.py` + +**Stretch Task** +Make the name property private, and add a get_name() method to make it read only. {{}} From 615f33c7f3f94fc82eb99c4f928d281f52ec1b0b Mon Sep 17 00:00:00 2001 From: l Date: Wed, 30 Sep 2026 11:29:36 +0100 Subject: [PATCH 13/33] Spelling pass 1 --- common-content/en/module/decomposition/generics/index.md | 4 ++-- common-content/en/module/decomposition/methods/index.md | 2 +- .../module/decomposition/type-guided-refactorings/index.md | 6 +++--- common-content/en/module/decomposition/why-types/index.md | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/common-content/en/module/decomposition/generics/index.md b/common-content/en/module/decomposition/generics/index.md index 861baf9ec..23b814f2a 100644 --- a/common-content/en/module/decomposition/generics/index.md +++ b/common-content/en/module/decomposition/generics/index.md @@ -127,7 +127,7 @@ Now that we've told mypy `FamilyTree.members` is a list of type `Person`, it can The kind of relationship structure we created with families and members, a {{}}A list could store numbers, or strings. We use generic types to say which type a particular instance of a list stores. Even though we can have a list of strings, and a list of numbers, the code for finding the first element is the same. But knowing that a list _only_ contains strings is useful.{{}}, is common across many types of data, for example how species of animal are related to each other, or how a dictionary might store words. -Thinking about keeping our code reusable, is there a way we could define such structures, and be able to force them to work with certain types, without needing to write a special class for each individual data type? Just like lists can takea generic to force them to be a certain type, we can write classes that accept generics. +Thinking about keeping our code reusable, is there a way we could define such structures, and be able to force them to work with certain types, without needing to write a special class for each individual data type? Just like lists can take a generic to force them to be a certain type, we can write classes that accept generics. Look at the following code: @@ -173,7 +173,7 @@ The Tree here has a special type annotation given by `T`. This is a generic, tel Observe that we then create two different trees: a `Tree` and a `Tree`. In these trees, the parent and list of children must contain `Person` and `Animal` types respectively. -It also means instead of having to create a new function torpint out every single tree type, we can create a single function - `Tree.print_tree()`. +It also means instead of having to create a new function to print out every single tree type, we can create a single function - `Tree.print_tree()`. {{}} **Task 12** diff --git a/common-content/en/module/decomposition/methods/index.md b/common-content/en/module/decomposition/methods/index.md index 3bff309bf..87417b02e 100644 --- a/common-content/en/module/decomposition/methods/index.md +++ b/common-content/en/module/decomposition/methods/index.md @@ -129,7 +129,7 @@ In python, any class property that begins with two underscores is considered _pr > > Using underscores, Python doesn't have a clear way of marking something as private. > Other programming languages like Java mark this more explicitly with keywords like "private" and "public". -> It's worth becoming familair with this private/public language even if you're not using it right now. +> It's worth becoming familiar with this private/public language even if you're not using it right now. > You can now program classes to change behaviour based on the information stored within them. diff --git a/common-content/en/module/decomposition/type-guided-refactorings/index.md b/common-content/en/module/decomposition/type-guided-refactorings/index.md index dbd6d5417..0f805e3f2 100644 --- a/common-content/en/module/decomposition/type-guided-refactorings/index.md +++ b/common-content/en/module/decomposition/type-guided-refactorings/index.md @@ -1,8 +1,8 @@ +++ -title = "Type-guided refactorings" +title = "Type-guided refactoring" time = 30 objectives = [ - "Explain how type annotations and type checking can guide refactorings.", + "Explain how type annotations and type checking can guide refactoring.", "Use mypy to guide a refactoring.", ] @@ -75,7 +75,7 @@ Try changing the type annotation of `Person.preferred_operating_system` from `st Run mypy on the code. -It tells us different places that our code is now wrong. Fix it to remov eany errors. +It tells us different places that our code is now wrong. Fix it to remove any errors. Now we changed the types, we probably also want to _rename_ our fields to something appropriate. diff --git a/common-content/en/module/decomposition/why-types/index.md b/common-content/en/module/decomposition/why-types/index.md index 4f4d84153..bf390500d 100644 --- a/common-content/en/module/decomposition/why-types/index.md +++ b/common-content/en/module/decomposition/why-types/index.md @@ -29,7 +29,7 @@ Then try running the file and see what happens. In that file, is `half("22")` hoping to return 11 (because the string should be converted to a number)? Or return 2 (because it's the first half of the string)? Or error, because it doesn't make sense? -What if we tried to run `half("hello")`? Try to give part of a word, or error because it can'tbe split evenly in half? Does this input even make sense? +What if we tried to run `half("hello")`? Try to give part of a word, or error because it can't be split evenly in half? Does this input even make sense? What if we did `double("hello")` instead? What do you expect it to return? How about `second(22)`? Should it treat 22 like a stringified version of the decimal representation of the number 22 and return 2? If so - `22` is the same as `0x16`. Should `second(0x16)` convert `0x16` to decimal before returning the second character? Or should it remember that the original number was input as hexadecimal and return `6`? From 85e6ee3fc490f423458819892970382fade21029 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 30 Sep 2026 12:37:43 +0100 Subject: [PATCH 14/33] language pass 2 --- .../classes-and-objects/index.md | 8 +-- .../module/decomposition/dataclasses/index.md | 10 ++-- .../en/module/decomposition/enums/index.md | 2 +- .../en/module/decomposition/generics/index.md | 52 +++---------------- .../module/decomposition/inheritance/index.md | 6 +-- .../en/module/decomposition/methods/index.md | 35 +++++++++---- .../type-checking-with-mypy/index.md | 8 ++- .../type-guided-refactorings/index.md | 4 +- .../module/decomposition/why-types/index.md | 4 +- .../content/sdc/tools/sprints/5/prep/index.md | 2 +- 10 files changed, 57 insertions(+), 74 deletions(-) diff --git a/common-content/en/module/decomposition/classes-and-objects/index.md b/common-content/en/module/decomposition/classes-and-objects/index.md index 262f157ba..079b764da 100644 --- a/common-content/en/module/decomposition/classes-and-objects/index.md +++ b/common-content/en/module/decomposition/classes-and-objects/index.md @@ -45,7 +45,7 @@ This code contains some untyped objects. Try checking it with mypy before running the code and predict what you think will happen when you run the code. {{}} -This code doesn't work, but mypy can't tell us this. Remember how we said that type checking has its limits? +The code in the above exercise doesn't work, but mypy can't tell us this. Remember how we said that type checking has its limits? As far as mypy is concerned, a dictionary is a dictionary - it could contain any keys! Instead, we can use a {{}}A class is a template for an object. It lets us say what properties (and methods) all instances of that class will contain.{{}}. @@ -81,8 +81,8 @@ The method called `__init__` is called a constructor - it is what is called when {{}} @@ -102,7 +102,7 @@ Have a look at file `06-classes.py`. Run mypy and fix any errors. -Add a new function called `likes_apple` which takes a person as parameter and returns true only if the preferred operating system is either `iOS` or `macOS`. Add all the appropriate type annotations and make sure mypy has no errors. +Add a new function called `likes_apple` which takes a `Person` as parameter and returns true only if the preferred operating system is either `iOS` or `macOS`. Add all the appropriate type annotations and make sure mypy has no errors. Compare objects and classes and explain some advantages and disadvantages of each. diff --git a/common-content/en/module/decomposition/dataclasses/index.md b/common-content/en/module/decomposition/dataclasses/index.md index 3f09b471f..1280c484c 100644 --- a/common-content/en/module/decomposition/dataclasses/index.md +++ b/common-content/en/module/decomposition/dataclasses/index.md @@ -19,7 +19,7 @@ Our `Person` class is an example of this. We just store some data in it (and may If a class is just a place to group related data, it is sometimes called a {{}}A value object is an object which exists just to store data. They are normally immutable (never change).{{}}. We normally consider two value objects to be equal to each other if their fields contain the same values. -There are several functions we can implement on classes that have obvious implementations for value objects. +There are several methods we can implement on classes that have obvious implementations for value objects. Equality is one: ideally two value objects are the same if their fields are the same. But this is not the case with objects by default: @@ -35,7 +35,7 @@ imran2 = Person("Imran", 22, "Ubuntu") print(imran == imran2) # Prints False ``` -Similarly, it's useful when we print a value object to see its type and fields. But this is not the case with objects by default: +Similarly, it's useful when we print a value object to see its type and properties. But this is not the case with objects by default: ```python class Person: @@ -48,7 +48,7 @@ imran = Person("Imran", 22, "Ubuntu") print(imran) # Prints <__main__.Person object at 0x1048b5a90> ``` -Python has a useful {{}}A decorator is an annotation you can add to some Python code to give it extra behaviour.{{}} called `dataclass` which generates some of these functions for us. In fact, it even generates the constructor for us. +Python has a useful {{}}A decorator is an annotation you can add to some Python code to give it extra behaviour.{{}} called `dataclass` which generates some of these methods for us. In fact, it even generates the constructor for us. ```python from dataclasses import dataclass @@ -75,9 +75,9 @@ Other languages have a similar idea of a value type, and tools to help make them **Task 10** -Work in file `09-implement.py` for this task. +Work in file `10-implement.py` for this task. -Convert your existing `Person` class into a value type using `@datatype` so you can print the class (and see it's type and fields) and compare class instances that are identical. Make sure your `is_adult` method and `drivers_license_check` free function both work as normal. +Convert your existing `Person` class into a value type using `@datatype` so you can print the class (and see it's type and properties) and compare class instances that are identical. Make sure your `is_adult` method and `drivers_license_check` free function both work as normal. Make a new method on your Person class - `greet` which should return `"Hello !"` when used. diff --git a/common-content/en/module/decomposition/enums/index.md b/common-content/en/module/decomposition/enums/index.md index 9bab1c011..2a16b6b0f 100644 --- a/common-content/en/module/decomposition/enums/index.md +++ b/common-content/en/module/decomposition/enums/index.md @@ -52,7 +52,7 @@ We know that when we save data, transfer it across a network, or take user input {{}} **Task 14** -Look at file `13-implement.py` +Look at file `14-implement.py` It currently handles operating systems as strings. diff --git a/common-content/en/module/decomposition/generics/index.md b/common-content/en/module/decomposition/generics/index.md index 23b814f2a..c2d398acc 100644 --- a/common-content/en/module/decomposition/generics/index.md +++ b/common-content/en/module/decomposition/generics/index.md @@ -17,44 +17,9 @@ objectives = [ Sometimes we want to reason about more complicated type relationships than "this field is a string". Lists and dicts are examples of this. We may want to reason that every value in a list is a string. -Consider this code: - -```python -from dataclasses import dataclass - -@dataclass(frozen=True) -class Animal: - name: str - species: str - -@dataclass(frozen=True) -class Person: - name: str - age: int - -@dataclass(frozen=True) -class FamilyTree: - parent: Person - members: list - -pet = Animal(name="Gromit", species="Dog") -fatma = Person(name="Fatma", age=4) -aisha = Person(name="Aisha", age=6) -imran = Person(name="Imran", age=30) - -family = FamilyTree(parent=imran, members=[fatma, aisha, pet]) - -def print_family_tree(family: FamilyTree): - print(family.parent.name) - for child in family.members: - print(f"{child.name} ({child.age} years old)") - -print_family_tree(family) -``` - {{}} **Task 11** -Have a look at the above code, you can find a copy in `10-predict.py` +Have a look at the code in `11-predict.py` There is a bug in this code. Can you spot it? @@ -67,7 +32,7 @@ In some languages, like Java, C#, Rust, or Go, type information is _required_ - In other languages, like Python and JavaScript, type information is _optional_. Because of this, tools that check types are sometimes less strict. If they don't know what type something has, they stop doing any checks. -That's what's happening here. `FamilyTree.members` is a `list`, but mypy doesn't know what type of thing is in the list. It doesn't even know that everything in the list has the same type = `["hello", 7, True]` is a legal list in Python. Many people would consider a pet to be a member of the family, so it seems correct, but due to the different types, this code breaks down and mypy can't spot the problem. +That's what's happening in task 11. `FamilyTree.members` is a `list`, but mypy doesn't know what type of thing is in the list. It doesn't even know that everything in the list has the same type = `["hello", 7, True]` is a legal list in Python. Many people would consider a pet to be a member of the family, so it seems correct, but due to the different types, this code breaks down and mypy can't spot the problem. ## Using Generics @@ -107,9 +72,7 @@ def print_family_tree(family: FamilyTree): print_family_tree(family) ``` -Try updating the code with this change and see if mypy spots the problem - -Run this code through mypy. +Try updating your code for task 11 with this change and see if mypy spots the problem Now that we've told mypy `FamilyTree.members` is a list of type `Person`, it can identify that the `child` variable printed out must be of type `Person`. Because of this, it can tell us that `child.age` on doesn't exist when the pet is accidentally included in the list. @@ -125,7 +88,7 @@ Now that we've told mypy `FamilyTree.members` is a list of type `Person`, it can ## Writing our own classes that use generics -The kind of relationship structure we created with families and members, a {{}}A list could store numbers, or strings. We use generic types to say which type a particular instance of a list stores. Even though we can have a list of strings, and a list of numbers, the code for finding the first element is the same. But knowing that a list _only_ contains strings is useful.{{}}, is common across many types of data, for example how species of animal are related to each other, or how a dictionary might store words. +The kind of relationship structure we created with families and members, a {{}}A list can store a linear array of values. A tree stores values in a heirarchy, like a family tree.{{}}, is common across many types of data, for example how species of animal are related to each other, or how a dictionary might store words. Thinking about keeping our code reusable, is there a way we could define such structures, and be able to force them to work with certain types, without needing to write a special class for each individual data type? Just like lists can take a generic to force them to be a certain type, we can write classes that accept generics. @@ -173,15 +136,16 @@ The Tree here has a special type annotation given by `T`. This is a generic, tel Observe that we then create two different trees: a `Tree` and a `Tree`. In these trees, the parent and list of children must contain `Person` and `Animal` types respectively. -It also means instead of having to create a new function to print out every single tree type, we can create a single function - `Tree.print_tree()`. +It also means instead of having to create a new method to print out every single tree type, we can create a single method - `Tree.print_tree()`. {{}} **Task 12** + Experiment with mypy and make sure that the family tree only takes `Person` types and the species tree only takes `Animal` types. -We are going to improve the printing in the above code, you can find a copy in `11-fix.py` +We are going to improve the printing in the above code, you can find a copy in `12-fix.py` -Currently the `Tree.print_tree()` function doesn't look very pretty. +Currently the `Tree.print_tree()` method doesn't look very pretty. Change the Animal and Person classes, using whichever approach you think is best, to allow the `Tree.print_tree()` method to display an output that looks like this: diff --git a/common-content/en/module/decomposition/inheritance/index.md b/common-content/en/module/decomposition/inheritance/index.md index 235155718..55e61dc45 100644 --- a/common-content/en/module/decomposition/inheritance/index.md +++ b/common-content/en/module/decomposition/inheritance/index.md @@ -101,7 +101,7 @@ The method implementations are different for the two classes. They have differen **Task 15** -A copy of this code is in file `14-analyse.py` +A copy of this code is in file `15-analyse.py` Try using this code and make sure you understand how it works and what it does @@ -117,7 +117,7 @@ Q2: If you know in advance you will be initialising many of them repeatedly, whi Q1: `SortedImmutableNumberList` sorts the numbers in advance, and the method implementation for largest item only needs to look at the final item of the sorted list. This means accessing it is faster. Q2: `ImmutableNumberList` doesn't need to sort the numbers immediately on creation. If you only intended to use `first` and `last`, it may be faster. -Of course, it all depends on which functions you think you will need. +Of course, it all depends on which functionality you think you will need. You will learn more about these efficiency concepts in the upcoming complexity module. @@ -131,7 +131,7 @@ Inheritance is a great way of helping you achieve this. {{}} **Task 16** -Look at file `15-playcomputer.py` +Look at file `16-playcomputer.py` Play computer with this code diff --git a/common-content/en/module/decomposition/methods/index.md b/common-content/en/module/decomposition/methods/index.md index 87417b02e..8543f128f 100644 --- a/common-content/en/module/decomposition/methods/index.md +++ b/common-content/en/module/decomposition/methods/index.md @@ -22,7 +22,7 @@ def is_adult(person: Person) -> bool: return person.age >= 18 ``` -We've also seen types that have methods on them, e.g. `"abc".upper()`. This looks a bit different from functions we define ourselves (which may look like `upper("abc")`). +We've also seen types that have methods on them, e.g. `"abc".upper()`. This looks a bit different from functions we define ourselves, e.g. `upper("abc")`. Methods are just like functions, but they are attached to a class. @@ -81,8 +81,8 @@ print(drivers_license_check(imran)) # returns 'Valid drivers license' Work inside the `08-implement.py` file for this task. -1. Add the `drivers_license_check` free function and the `is_adult` method into your code, and make sure your code currently gives the expected output. -1. Change the `Person` class to take a date of birth (using [the standard library's `datetime.date` class](https://docs.python.org/3/library/datetime.html#datetime.date)) and store the `date of birth` in a field instead of `age` (it should be a `str`). Don't change anything else. +1. Add the `drivers_license_check` free function and an `is_adult` method into your code, and make sure your code currently gives the expected output. +1. Change the `Person` class to take a date of birth (using [the standard library's `datetime.date` class](https://docs.python.org/3/library/datetime.html#datetime.date)) and store the `date of birth` in a property instead of `age` (it should be a `str`). Don't change anything else. 1. **Try to run your code**, how does this change break your code. What kind of error do you get? Is it helpful in identifying where your next change needs to be? 1. Update the `is_adult` method so the error is fixed. Using the `drivers_license_check` function check everything runs as expected, it should return "Valid drivers license". _You should not change `drivers_license_check`_. {{}} @@ -123,10 +123,18 @@ print(eliza.name) print(eliza.is_adult()) # works and prints False ``` -In python, any class property that begins with two underscores is considered _private_, i.e. it can only be used within that specific class instance. - > [!NOTE] +> +> It is important to be clear about the wording here as there are some subtle differences between fields and properties as used in classes. +> A "field" is the underlying part of a class that stores some value. +> A "property" is the publicly accessible part that you can access from outside the class. > + +In python, any field that begins with two underscores is considered _private_, i.e. it can only be used within that specific class instance. + + +> [!NOTE] +> > Using underscores, Python doesn't have a clear way of marking something as private. > Other programming languages like Java mark this more explicitly with keywords like "private" and "public". > It's worth becoming familiar with this private/public language even if you're not using it right now. @@ -137,24 +145,29 @@ Compare this with objects, which can only ever store data, and behave the same e Another benefit of encapsulation is letting you make "read only" properties. Think about the example above. -Imagine you wanted to check if a `Person` class had a certain name using an equality test, but accidentally only used a single `=` symbol: +Imagine you wanted to check if a `Person` class had a certain name using an equality test, but accidentally used a single `=` symbol: ```python imran.name = "Eliza" ``` -Python allows you to change properties whenever you want. +Python allows you to update public fields whenever you want. If `name` were private, and the only way to access it was through a `get_name()` method that returns a string, it would be impossible to accidentally change the value. In this way, encapsulation can be used to prevent accidental errors in code. +{{}} +Read through [Python encapsulation](https://www.w3schools.com/python/python_encapsulation.asp). + +Do some further research of your own to learn about encapsulation. +{{}} + {{}} **Task 9** -Start by reading [Python encapsulation](https://www.w3schools.com/python/python_encapsulation.asp) and think about some of the benefits that encapsulation can add to a class. - -Do some further research of your own to learn about encapsulation. +Having done some research on encapsulation, think about the benefits. Think of some examples and in your own words write down some benefits and trade-offs of using encapsulation in classes in the file `09-encapsulation.py` **Stretch Task** -Make the name property private, and add a get_name() method to make it read only. + +Make the `name` field private, and add a `get_name()` method to allow read-only access. {{}} diff --git a/common-content/en/module/decomposition/type-checking-with-mypy/index.md b/common-content/en/module/decomposition/type-checking-with-mypy/index.md index 34c964cba..2a7afcf8c 100644 --- a/common-content/en/module/decomposition/type-checking-with-mypy/index.md +++ b/common-content/en/module/decomposition/type-checking-with-mypy/index.md @@ -39,5 +39,11 @@ Have a look at `04-addmypy.py` This code contains bugs related to types. They are bugs mypy can catch. -Read this code to understand what it's trying to do. Add type annotations to the method parameters and return types of this code. Run the code through mypy, and fix all of the bugs that show up. When you're confident all of the type annotations are correct, and the bugs are fixed, run the code and check it works. +Read this code to understand what it's trying to do. + +Add type annotations to the method parameters and return types of this code. + +Run the code through mypy, and fix all of the bugs that show up. + +When you're confident all of the type annotations are correct, and the bugs are fixed, run the code and check it works. {{}} diff --git a/common-content/en/module/decomposition/type-guided-refactorings/index.md b/common-content/en/module/decomposition/type-guided-refactorings/index.md index 0f805e3f2..8b22619a0 100644 --- a/common-content/en/module/decomposition/type-guided-refactorings/index.md +++ b/common-content/en/module/decomposition/type-guided-refactorings/index.md @@ -69,7 +69,7 @@ Let's imagine we want to change our code. We don't want to say "Every person has {{}} **Task 13** -A copy of this file is present in `12-refactor.py`. +A copy of this file is present in `13-refactor.py`. Try changing the type annotation of `Person.preferred_operating_system` from `str` to `List[str]`. @@ -77,7 +77,7 @@ Run mypy on the code. It tells us different places that our code is now wrong. Fix it to remove any errors. -Now we changed the types, we probably also want to _rename_ our fields to something appropriate. +Now we changed the types, we probably also want to _rename_ our field to something appropriate. Run mypy again. diff --git a/common-content/en/module/decomposition/why-types/index.md b/common-content/en/module/decomposition/why-types/index.md index bf390500d..1957d7152 100644 --- a/common-content/en/module/decomposition/why-types/index.md +++ b/common-content/en/module/decomposition/why-types/index.md @@ -15,12 +15,12 @@ objectives = [ In real life, as well as programming, there are some impossible operations. Can you divide seven by yellow? Can you set fire to a sound? These don't make sense. The same is true in programming. -We are going to look at some functions which you can find in the file `SDC-Tools/sprint-5` directory. +Throughout this sprint we are going to look at some code which you can find in the file `SDC-Tools/sprint-5` directory of [the Module-Tools repository](https://github.com/CodeYourFuture/Module-Tools). {{}} **Task 1** -Have a look now at `01-predict.py`. +Have a look at `01-predict.py`. Take a moment to make predictions about what function calls will and will not work. diff --git a/org-cyf/content/sdc/tools/sprints/5/prep/index.md b/org-cyf/content/sdc/tools/sprints/5/prep/index.md index a8ba8773f..8823c4e81 100644 --- a/org-cyf/content/sdc/tools/sprints/5/prep/index.md +++ b/org-cyf/content/sdc/tools/sprints/5/prep/index.md @@ -22,7 +22,7 @@ src = "module/decomposition/dataclasses" name = "Generics" src = "module/decomposition/generics" [[blocks]] -name = "Type-guided refactorings" +name = "Type-guided refactoring" src = "module/decomposition/type-guided-refactorings" [[blocks]] name = "enums" From d4d6774afcfb8985ed82ccaadc21389e083c9421 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 30 Sep 2026 13:00:55 +0100 Subject: [PATCH 15/33] language pass 3 --- .../module/decomposition/dataclasses/index.md | 2 +- .../en/module/decomposition/generics/index.md | 38 ++------ .../module/decomposition/inheritance/index.md | 78 +--------------- .../en/module/decomposition/methods/index.md | 90 +------------------ .../type-guided-refactorings/index.md | 49 +--------- .../content/sdc/tools/sprints/5/prep/index.md | 5 +- 6 files changed, 19 insertions(+), 243 deletions(-) diff --git a/common-content/en/module/decomposition/dataclasses/index.md b/common-content/en/module/decomposition/dataclasses/index.md index 1280c484c..93b313d4d 100644 --- a/common-content/en/module/decomposition/dataclasses/index.md +++ b/common-content/en/module/decomposition/dataclasses/index.md @@ -77,7 +77,7 @@ Other languages have a similar idea of a value type, and tools to help make them Work in file `10-implement.py` for this task. -Convert your existing `Person` class into a value type using `@datatype` so you can print the class (and see it's type and properties) and compare class instances that are identical. Make sure your `is_adult` method and `drivers_license_check` free function both work as normal. +Convert your existing `Person` class from task 8 into a value type using `@datatype` so you can print the class (and see it's type and properties) and compare class instances that are identical. Make sure your `is_adult` method and `drivers_license_check` free function both work as normal. Make a new method on your Person class - `greet` which should return `"Hello !"` when used. diff --git a/common-content/en/module/decomposition/generics/index.md b/common-content/en/module/decomposition/generics/index.md index c2d398acc..90aaba852 100644 --- a/common-content/en/module/decomposition/generics/index.md +++ b/common-content/en/module/decomposition/generics/index.md @@ -36,40 +36,15 @@ That's what's happening in task 11. `FamilyTree.members` is a `list`, but mypy d ## Using Generics -We can use {{}}A list could store numbers, or strings. We use generic types to say which type a particular instance of a list stores. Even though we can have a list of strings, and a list of numbers, the code for finding the first element is the same. But knowing that a list _only_ contains strings is useful.{{}} to tell mypy what type of thing is in the list: +We can use {{}}A list could store numbers, or strings. We use generic types to say which type a particular instance of a list stores. Even though we can have a list of strings, and a list of numbers, the code for finding the first element is the same. But knowing that a list _only_ contains strings is useful.{{}} to tell mypy what type of thing is in the list. We could add an import and modify the `FamilyTree` class from task 11 as follows: ```python -from dataclasses import dataclass from typing import List -@dataclass(frozen=True) -class Animal: - name: str - species: str - -@dataclass(frozen=True) -class Person: - name: str - age: int - @dataclass(frozen=True) class FamilyTree: parent: Person members: List[Person] - -pet = Animal(name="Gromit", species="Dog") -fatma = Person(name="Fatma", age=4) -aisha = Person(name="Aisha", age=6) -imran = Person(name="Imran", age=30) - -family = FamilyTree(parent=imran, members=[fatma, aisha, pet]) - -def print_family_tree(family: FamilyTree): - print(family.parent.name) - for child in family.members: - print(f"{child.name} ({child.age} years old)") - -print_family_tree(family) ``` Try updating your code for task 11 with this change and see if mypy spots the problem @@ -79,9 +54,8 @@ Now that we've told mypy `FamilyTree.members` is a list of type `Person`, it can > [!NOTE] > -> Most generics don't need the types to be quoted. For example, you can write `List[Person]`. -> But if you want to recursively reference a type within the class, before the class has been defined, we need to quote it for mypy to recognise it. -> So for example, if we wanted a family tree to go several levels deep, e.g. to include grandchildren, we would write it as `List["FamilyTree"]`. +> I you want to _recursively_ reference a type within a class, we need to quote it for mypy to recognise it. +> So for example, if we wanted a `Person` object to include a list of children, we would write it as `List["Person"]`. > > It's kind of annoying, but don't worry about it too much. @@ -141,13 +115,13 @@ It also means instead of having to create a new method to print out every single {{}} **Task 12** -Experiment with mypy and make sure that the family tree only takes `Person` types and the species tree only takes `Animal` types. +We are going to improve the printing in the above code, you can find a copy in `12-fix.py`. -We are going to improve the printing in the above code, you can find a copy in `12-fix.py` +Experiment with mypy and make sure that the family tree only takes `Person` types and the species tree only takes `Animal` types. Currently the `Tree.print_tree()` method doesn't look very pretty. -Change the Animal and Person classes, using whichever approach you think is best, to allow the `Tree.print_tree()` method to display an output that looks like this: +Change only the Animal and Person classes to allow the `Tree.print_tree()` method to display an output that looks like this: ``` Imran (30 years old) diff --git a/common-content/en/module/decomposition/inheritance/index.md b/common-content/en/module/decomposition/inheritance/index.md index 55e61dc45..0376f8841 100644 --- a/common-content/en/module/decomposition/inheritance/index.md +++ b/common-content/en/module/decomposition/inheritance/index.md @@ -18,81 +18,7 @@ In this prep we have seen how add methods to classes to encapsulate functionalit Classes can _extend_ other classes to share most of their functionality but add or replace some of it. A class that carries over something from another class is called _inheritance_. -Read the following code: - -```python -from typing import Iterable, Optional - -class ImmutableNumberList: - # We accept any `Iterable[int]` here, so can construct with a list, a set, or anything else that can be iterated. - def __init__(self, elements: Iterable[int]): - # We copy the elements so that if someone mutates the passed in elements list, our copy won't be mutated. - self.elements = [element for element in elements] - - def first(self) -> Optional[int]: - if not self.elements: - return None - return self.elements[0] - - def last(self) -> Optional[int]: - if not self.elements: - return None - return self.elements[-1] - - def length(self) -> int: - return len(self.elements) - - def largest(self) -> Optional[int]: - # To find the largest element, we need to go through the entire list (which may take some time). - if not self.elements: - return None - largest = self.elements[0] - for element in self.elements: - if element > largest: - largest = element - return largest - - -# A SortedImmutableNumberList is the same as an ImmutableNumberList, -# but it changes some aspects. -class SortedImmutableNumberList(ImmutableNumberList): - def __init__(self, elements: Iterable[int]): - # We do extra work here when constructing the list, - # to make sure the elements are sorted. - # This takes more time than the ImmutableNumberList version would. - super().__init__(sorted(elements)) - - # This method overrides (replaces) the method with the same name on the super-class. - def largest(self) -> Optional[int]: - # Because we know the elements were already sorted in the constructor, - # we can implement finding the largest number faster. - # We don't need to look through every element - we know the largest element is at the end. - # Because we did extra work one time before (in the constructor), - # we can avoid re-doing that work every time someone calls `largest()`. - return self.last() - - def max_gap_between_values(self) -> Optional[int]: - if not self.elements: - return None - previous_element = None - max_gap = -1 - for element in self.elements: - if previous_element is not None: - gap = element - previous_element - if gap > max_gap: - max_gap = gap - previous_element = element - return max_gap - - -values = SortedImmutableNumberList([1, 19, 7, 13, 4]) -print(values.largest()) -print(values.max_gap_between_values()) - -unsorted_values = ImmutableNumberList([1, 19, 7, 13, 4]) -print(unsorted_values.largest()) -print(unsorted_values.max_gap_between_values()) # This doesn't work - the superclass doesn't define this method. -``` +Read the code in file `15-analyse.py`. We have two classes that behave the same. They both have a constructor, and four methods (`first`, `last`, `largest`, `length`). `SortedImmutableNumberList` also has an extra method: `max_gap_between_values` which `ImmutableNumberList` does not have. The method implementations are different for the two classes. They have different trade-offs to consider. @@ -101,7 +27,7 @@ The method implementations are different for the two classes. They have differen **Task 15** -A copy of this code is in file `15-analyse.py` +Work in file `15-analyse.py` Try using this code and make sure you understand how it works and what it does diff --git a/common-content/en/module/decomposition/methods/index.md b/common-content/en/module/decomposition/methods/index.md index 8543f128f..851b982b8 100644 --- a/common-content/en/module/decomposition/methods/index.md +++ b/common-content/en/module/decomposition/methods/index.md @@ -6,7 +6,6 @@ objectives = [ "Define a free function.", "Explain why methods can be more useful than free functions.", "Amend a method on a class.", - "Explain how encapsulation can benefit class design.", ] [build] @@ -81,93 +80,12 @@ print(drivers_license_check(imran)) # returns 'Valid drivers license' Work inside the `08-implement.py` file for this task. -1. Add the `drivers_license_check` free function and an `is_adult` method into your code, and make sure your code currently gives the expected output. -1. Change the `Person` class to take a date of birth (using [the standard library's `datetime.date` class](https://docs.python.org/3/library/datetime.html#datetime.date)) and store the `date of birth` in a property instead of `age` (it should be a `str`). Don't change anything else. -1. **Try to run your code**, how does this change break your code. What kind of error do you get? Is it helpful in identifying where your next change needs to be? -1. Update the `is_adult` method so the error is fixed. Using the `drivers_license_check` function check everything runs as expected, it should return "Valid drivers license". _You should not change `drivers_license_check`_. -{{}} - - - -## Encapsulation - -An advantage of classes over objects is encapsulation. - -Imagine you have written your Person class that stores age information. - -For privacy reasons, you don't want to reveal the exact age of the person, only whether they are or are not over 18 years old. - -This means we need to store the "age" value in a class, but somehow keep it _private_ to that class. The only _public_ information we want is whether or not they are over 18. How can we achieve this? - -Look at the following code: - -```python -class Person: - def __init__(self, name: str, age: int): - self.name = name - self.__age = age - - def is_adult(self): - return self.__age >= 18 - -imran = Person("Imran", 22) -print(imran.name) -# print(imran.age) # fails -# print(imran.__age) # fails -print(imran.is_adult()) # works and prints True - -eliza = Person("Eliza", 12) -print(eliza.name) -# print(eliza.age) # fails -# print(imran.__age) # fails -print(eliza.is_adult()) # works and prints False -``` - -> [!NOTE] -> -> It is important to be clear about the wording here as there are some subtle differences between fields and properties as used in classes. -> A "field" is the underlying part of a class that stores some value. -> A "property" is the publicly accessible part that you can access from outside the class. -> - -In python, any field that begins with two underscores is considered _private_, i.e. it can only be used within that specific class instance. - - -> [!NOTE] -> -> Using underscores, Python doesn't have a clear way of marking something as private. -> Other programming languages like Java mark this more explicitly with keywords like "private" and "public". -> It's worth becoming familiar with this private/public language even if you're not using it right now. -> - -You can now program classes to change behaviour based on the information stored within them. -Compare this with objects, which can only ever store data, and behave the same every time. - -Another benefit of encapsulation is letting you make "read only" properties. -Think about the example above. -Imagine you wanted to check if a `Person` class had a certain name using an equality test, but accidentally used a single `=` symbol: -```python -imran.name = "Eliza" -``` -Python allows you to update public fields whenever you want. -If `name` were private, and the only way to access it was through a `get_name()` method that returns a string, it would be impossible to accidentally change the value. -In this way, encapsulation can be used to prevent accidental errors in code. - -{{}} -Read through [Python encapsulation](https://www.w3schools.com/python/python_encapsulation.asp). - -Do some further research of your own to learn about encapsulation. -{{}} - -{{}} -**Task 9** - -Having done some research on encapsulation, think about the benefits. +Add an `is_adult` method into the class, and make sure your code gives the expected output. -Think of some examples and in your own words write down some benefits and trade-offs of using encapsulation in classes in the file `09-encapsulation.py` +Change the `Person` class to take a date of birth (using [the standard library's `datetime.date` class](https://docs.python.org/3/library/datetime.html#datetime.date)) and store the `date of birth` instead of `age`. -**Stretch Task** +**Try to run your code now** and observe how this change breaks your code. What kind of error do you get? Is it helpful in identifying where your next change needs to be? -Make the `name` field private, and add a `get_name()` method to allow read-only access. +Now update _only_ the `is_adult` method to fix the error and check everything works correctly. {{}} diff --git a/common-content/en/module/decomposition/type-guided-refactorings/index.md b/common-content/en/module/decomposition/type-guided-refactorings/index.md index 8b22619a0..e524da278 100644 --- a/common-content/en/module/decomposition/type-guided-refactorings/index.md +++ b/common-content/en/module/decomposition/type-guided-refactorings/index.md @@ -18,52 +18,7 @@ We previously saw that using methods instead of free functions can help us to en Type checking can help us with this. If you have some code which accesses `imran.age`, and we remove the `age` field, we can run mypy: It will tell us "Here are all of the places that reference `age` you also need to change your code". -Take this file as an example. It is a program that works out what laptops could be allocated to what people based on their preferred operating system. - -```python -from dataclasses import dataclass -from typing import List - -@dataclass(frozen=True) -class Person: - name: str - age: int - preferred_operating_system: str - - -@dataclass(frozen=True) -class Laptop: - id: int - manufacturer: str - model: str - screen_size_in_inches: float - operating_system: str - - -def find_possible_laptops(laptops: List[Laptop], person: Person) -> List[Laptop]: - possible_laptops = [] - for laptop in laptops: - if laptop.operating_system == person.preferred_operating_system: - possible_laptops.append(laptop) - return possible_laptops - - -people = [ - Person(name="Imran", age=22, preferred_operating_system="Ubuntu"), - Person(name="Eliza", age=34, preferred_operating_system="Arch Linux"), -] - -laptops = [ - Laptop(id=1, manufacturer="Dell", model="XPS", screen_size_in_inches=13, operating_system="Arch Linux"), - Laptop(id=2, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system="Ubuntu"), - Laptop(id=3, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system="ubuntu"), - Laptop(id=4, manufacturer="Apple", model="macBook", screen_size_in_inches=13, operating_system="macOS"), -] - -for person in people: - possible_laptops = find_possible_laptops(laptops, person) - print(f"Possible laptops for {person.name}: {possible_laptops}") -``` +Look at file `13-refactor.py` as an example. It is a program that works out what laptops could be allocated to what people based on their preferred operating system. Let's imagine we want to change our code. We don't want to say "Every person has one preferred operating system" any more. We want to let people have a list of operating systems they prefer (in order). So we could say "Imran prefers Ubuntu most of all, and then Arch Linux, but will not use macOS". @@ -71,7 +26,7 @@ Let's imagine we want to change our code. We don't want to say "Every person has **Task 13** A copy of this file is present in `13-refactor.py`. -Try changing the type annotation of `Person.preferred_operating_system` from `str` to `List[str]`. +Change the type annotation of `Person.preferred_operating_system` from `str` to `List[str]`. Run mypy on the code. diff --git a/org-cyf/content/sdc/tools/sprints/5/prep/index.md b/org-cyf/content/sdc/tools/sprints/5/prep/index.md index 8823c4e81..493665f26 100644 --- a/org-cyf/content/sdc/tools/sprints/5/prep/index.md +++ b/org-cyf/content/sdc/tools/sprints/5/prep/index.md @@ -16,6 +16,9 @@ src = "module/decomposition/classes-and-objects" name = "Methods" src = "module/decomposition/methods" [[blocks]] +name = "Encapsulation" +src = "module/decomposition/encapsulation" +[[blocks]] name = "Dataclasses" src = "module/decomposition/dataclasses" [[blocks]] @@ -25,7 +28,7 @@ src = "module/decomposition/generics" name = "Type-guided refactoring" src = "module/decomposition/type-guided-refactorings" [[blocks]] -name = "enums" +name = "Enums" src = "module/decomposition/enums" [[blocks]] name = "Inheritance" From eabce4c572c363e75676bd7fead98041f322ede8 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 30 Sep 2026 13:16:00 +0100 Subject: [PATCH 16/33] Make tasks consistent --- .../en/module/decomposition/dataclasses/index.md | 4 +++- .../en/module/decomposition/methods/index.md | 12 +++--------- .../en/module/decomposition/why-types/index.md | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/common-content/en/module/decomposition/dataclasses/index.md b/common-content/en/module/decomposition/dataclasses/index.md index 93b313d4d..d7ad233f0 100644 --- a/common-content/en/module/decomposition/dataclasses/index.md +++ b/common-content/en/module/decomposition/dataclasses/index.md @@ -77,9 +77,11 @@ Other languages have a similar idea of a value type, and tools to help make them Work in file `10-implement.py` for this task. +Copy what you have done so far from task 8 into this file. + Convert your existing `Person` class from task 8 into a value type using `@datatype` so you can print the class (and see it's type and properties) and compare class instances that are identical. Make sure your `is_adult` method and `drivers_license_check` free function both work as normal. Make a new method on your Person class - `greet` which should return `"Hello !"` when used. -Take a look at the [`@datatype` documentation](https://docs.python.org/3/library/dataclasses.html) - what does `frozen=True` do to the class? What other options could you play around with and explore? +Read the [`@datatype` documentation](https://docs.python.org/3/library/dataclasses.html). Explain what `frozen=True` does to the class? What other options could you play around with and explore? {{}} diff --git a/common-content/en/module/decomposition/methods/index.md b/common-content/en/module/decomposition/methods/index.md index 851b982b8..07d804aa8 100644 --- a/common-content/en/module/decomposition/methods/index.md +++ b/common-content/en/module/decomposition/methods/index.md @@ -49,17 +49,11 @@ This has a few advantages over {{= 18 + +imran = Person("Imran", 22) +print(imran.name) +# print(imran.age) # fails +# print(imran.__age) # fails +print(imran.is_adult()) # works and prints True + +eliza = Person("Eliza", 12) +print(eliza.name) +# print(eliza.age) # fails +# print(imran.__age) # fails +print(eliza.is_adult()) # works and prints False +``` + +> [!NOTE] +> +> It is important to be clear about the wording here as there are some subtle differences between fields and properties as used in classes. +> A "field" is the underlying part of a class that stores some value. +> A "property" is the publicly accessible part that you can access from outside the class. +> + +In python, any field that begins with two underscores is considered _private_, i.e. it can only be used within that specific class instance. + + +> [!NOTE] +> +> Using underscores, Python doesn't have a clear way of marking something as private. +> Other programming languages like Java mark this more explicitly with keywords like "private" and "public". +> It's worth becoming familiar with this private/public language even if you're not using it right now. +> + +You can now program classes to change behaviour based on the information stored within them. +Compare this with objects, which can only ever store data, and behave the same every time. + +Another benefit of encapsulation is letting you make "read only" properties. +Think about the example above. +Imagine you wanted to check if a `Person` class had a certain name using an equality test, but accidentally used a single `=` symbol: +```python +imran.name = "Eliza" +``` +Python allows you to update public fields whenever you want. +If `name` were private, and the only way to access it was through a `get_name()` method that returns a string, it would be impossible to accidentally change the value. +In this way, encapsulation can be used to prevent accidental errors in code. + +{{}} +Read through [Python encapsulation](https://www.w3schools.com/python/python_encapsulation.asp). + +Do some further research of your own to learn about encapsulation. +{{}} + +{{}} +**Task 9** + +Having done some research on encapsulation, think about the benefits. + +Think of some examples and in your own words write down some benefits and trade-offs of using encapsulation in classes in the file `09-encapsulation.py` + +**Stretch Task** + +Working in file `09-encapsulation.py`, make the `name` field private, and add a `get_name()` method to allow read-only access. +{{}} + From 675a451e54a423f45c66c42349036b60addd19a2 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 30 Sep 2026 15:07:36 +0100 Subject: [PATCH 18/33] formatting --- .../en/module/decomposition/encapsulation/index.md | 1 - common-content/en/module/decomposition/generics/index.md | 6 +++--- common-content/en/module/decomposition/methods/index.md | 1 - .../decomposition/type-checking-with-mypy/index.md | 6 +++--- .../decomposition/type-guided-refactorings/index.md | 4 ++-- .../en/module/decomposition/why-types/index.md | 9 ++++----- 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/common-content/en/module/decomposition/encapsulation/index.md b/common-content/en/module/decomposition/encapsulation/index.md index be58f5574..98f525981 100644 --- a/common-content/en/module/decomposition/encapsulation/index.md +++ b/common-content/en/module/decomposition/encapsulation/index.md @@ -92,4 +92,3 @@ Think of some examples and in your own words write down some benefits and trade- Working in file `09-encapsulation.py`, make the `name` field private, and add a `get_name()` method to allow read-only access. {{}} - diff --git a/common-content/en/module/decomposition/generics/index.md b/common-content/en/module/decomposition/generics/index.md index 90aaba852..2b9bcc146 100644 --- a/common-content/en/module/decomposition/generics/index.md +++ b/common-content/en/module/decomposition/generics/index.md @@ -13,7 +13,7 @@ objectives = [ render = "never" +++ -## A problem type checking can't spot +### A problem type checking can't spot Sometimes we want to reason about more complicated type relationships than "this field is a string". Lists and dicts are examples of this. We may want to reason that every value in a list is a string. @@ -34,7 +34,7 @@ In other languages, like Python and JavaScript, type information is _optional_. That's what's happening in task 11. `FamilyTree.members` is a `list`, but mypy doesn't know what type of thing is in the list. It doesn't even know that everything in the list has the same type = `["hello", 7, True]` is a legal list in Python. Many people would consider a pet to be a member of the family, so it seems correct, but due to the different types, this code breaks down and mypy can't spot the problem. -## Using Generics +### Using Generics We can use {{}}A list could store numbers, or strings. We use generic types to say which type a particular instance of a list stores. Even though we can have a list of strings, and a list of numbers, the code for finding the first element is the same. But knowing that a list _only_ contains strings is useful.{{}} to tell mypy what type of thing is in the list. We could add an import and modify the `FamilyTree` class from task 11 as follows: @@ -60,7 +60,7 @@ Now that we've told mypy `FamilyTree.members` is a list of type `Person`, it can > It's kind of annoying, but don't worry about it too much. -## Writing our own classes that use generics +### Writing our own classes that use generics The kind of relationship structure we created with families and members, a {{}}A list can store a linear array of values. A tree stores values in a heirarchy, like a family tree.{{}}, is common across many types of data, for example how species of animal are related to each other, or how a dictionary might store words. diff --git a/common-content/en/module/decomposition/methods/index.md b/common-content/en/module/decomposition/methods/index.md index 07d804aa8..ff7471426 100644 --- a/common-content/en/module/decomposition/methods/index.md +++ b/common-content/en/module/decomposition/methods/index.md @@ -82,4 +82,3 @@ Change the `Person` class to take a date of birth (using [the standard library's Now update _only_ the `is_adult` method to fix the error and check everything works correctly. {{}} - diff --git a/common-content/en/module/decomposition/type-checking-with-mypy/index.md b/common-content/en/module/decomposition/type-checking-with-mypy/index.md index 2a7afcf8c..8eec46f2e 100644 --- a/common-content/en/module/decomposition/type-checking-with-mypy/index.md +++ b/common-content/en/module/decomposition/type-checking-with-mypy/index.md @@ -12,7 +12,7 @@ objectives = [ render = "never" +++ -## Support for type checking +### Support for type checking Different languages have different levels of support for checking types. @@ -24,8 +24,8 @@ Some very low level machine languages like assembly don't have any typing at all Languages with optional type checking perform good checks when you add this type information. If you don't add type annotations in your code, they will perform fewer checks. Sometimes they will infer the correct types based on what you _have_ annotated. Other times they will just ignore code with no annotations and not give you errors about it even if it's wrong. -## Trying out Mypy -Mypy is a tool which enables type checking in Python code. +### Trying out Mypy +Mypy is a tool which enables type checking in Python code. It works best if you can build up a habit of integrating it into your workflow. {{}} Read the first sections of [The Comprehensive Guide to mypy](https://dev.to/tusharsadhwani/the-comprehensive-guide-to-mypy-561m) up to and including the "Any type" section. diff --git a/common-content/en/module/decomposition/type-guided-refactorings/index.md b/common-content/en/module/decomposition/type-guided-refactorings/index.md index e524da278..0b40b04b3 100644 --- a/common-content/en/module/decomposition/type-guided-refactorings/index.md +++ b/common-content/en/module/decomposition/type-guided-refactorings/index.md @@ -22,6 +22,8 @@ Look at file `13-refactor.py` as an example. It is a program that works out what Let's imagine we want to change our code. We don't want to say "Every person has one preferred operating system" any more. We want to let people have a list of operating systems they prefer (in order). So we could say "Imran prefers Ubuntu most of all, and then Arch Linux, but will not use macOS". +The bigger (and more complicated) our codebase is, the more useful it is that mypy tells us what code needs changing. This is even more useful when we start working with code we didn't write ourselves, or we wrote long ago. Instead of needing to read all of the code and search around to try to work out where we need to change an `age` to `date_of_birth`, or how to access a single variable that has become a list of many, mypy can tell us "here are all of the places that are wrong". + {{}} **Task 13** A copy of this file is present in `13-refactor.py`. @@ -40,5 +42,3 @@ Fix all of the places that mypy tells you need changing. Then, make sure the program works as you'd expect. {{}} - -The bigger (and more complicated) our codebase is, the more useful it is that mypy tells us what code needs changing. This is even more useful when we start working with code we didn't write ourselves, or we wrote long ago. Instead of needing to read all of the code and search around to try to work out where we need to change an `age` to `date_of_birth`, or how to access a single variable that has become a list of many, mypy can tell us "here are all of the places that are wrong". diff --git a/common-content/en/module/decomposition/why-types/index.md b/common-content/en/module/decomposition/why-types/index.md index fa8fdd566..b72f3faf2 100644 --- a/common-content/en/module/decomposition/why-types/index.md +++ b/common-content/en/module/decomposition/why-types/index.md @@ -34,7 +34,7 @@ What if we did `double("hello")` instead? What do you expect it to return? How about `second(22)`? Should it treat 22 like a stringified version of the decimal representation of the number 22 and return 2? If so - `22` is the same as `0x16`. Should `second(0x16)` convert `0x16` to decimal before returning the second character? Or should it remember that the original number was input as hexadecimal and return `6`? -## Intent +### Intent The _intent_ of these functions is probably that `half` and `double` are expected to operate on numbers, and `second` is expected to operate on strings (and/or maybe lists). We don't know for sure what the author intended just by looking at the function names. @@ -74,7 +74,7 @@ How easy was it to spot this bug in your testing? The code in this file was wrong. It could never have been correct. After a `fetch`, `response.body.toLowerCase()` _never_ makes sense. Ideally we shouldn't have needed to wait until running the code, and using that exact input, to find this out. -## Types +### Types This is where types come in. @@ -83,9 +83,9 @@ Imagine if we could analyse our code and find out "You're calling `double` with We wouldn't need to keep executing our program with lots of different inputs every time we change it. The type analysis could tell us "You have a bug here, you should fix it". Without having to run the program, and without having to think about different possible inputs. -## Limits of type checking +### Limits of type checking -Types can be really useful for detecting bugs. But there are limits to what kind of bugs type checking can detect. +Types can be really useful for detecting bugs. But there are limits to what kind of bugs type checking can detect. {{}} **Task 3**: @@ -101,4 +101,3 @@ Are there multiple ways you could fix it? Type checking can't catch this type of bug - as long as you give it a number as input, it gives you a number as output. All of the types are correct. Not all bugs are type errors. But checking for type errors can get rid of a lot of them. - From 39cc20b387f158e2a52e99d3e12916a4d99ed7f8 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 30 Sep 2026 15:11:19 +0100 Subject: [PATCH 19/33] LOs --- common-content/en/module/decomposition/enums/index.md | 2 +- common-content/en/module/decomposition/methods/index.md | 2 +- common-content/en/module/decomposition/why-types/index.md | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/common-content/en/module/decomposition/enums/index.md b/common-content/en/module/decomposition/enums/index.md index 2a16b6b0f..2ecf318cd 100644 --- a/common-content/en/module/decomposition/enums/index.md +++ b/common-content/en/module/decomposition/enums/index.md @@ -5,7 +5,7 @@ objectives = [ "Identify risks of using strings to represent data.", "Define an enum.", "Explain how an enum addresses the risks of using strings to represent data.", - "Write code which checks string validity once, and then uses type-checking to avoid further validity checks.", + "Write code which validates input and then uses appropriate enums.", ] [build] diff --git a/common-content/en/module/decomposition/methods/index.md b/common-content/en/module/decomposition/methods/index.md index ff7471426..8e3d39d6b 100644 --- a/common-content/en/module/decomposition/methods/index.md +++ b/common-content/en/module/decomposition/methods/index.md @@ -5,7 +5,7 @@ objectives = [ "Define a method.", "Define a free function.", "Explain why methods can be more useful than free functions.", - "Amend a method on a class.", + "Add a method to a class.", ] [build] diff --git a/common-content/en/module/decomposition/why-types/index.md b/common-content/en/module/decomposition/why-types/index.md index b72f3faf2..6a7549c06 100644 --- a/common-content/en/module/decomposition/why-types/index.md +++ b/common-content/en/module/decomposition/why-types/index.md @@ -2,6 +2,7 @@ title = "Why we use types" time = 30 objectives = [ + "Explain what a type is.", "Explain how type annotations help understand a function's expectations.", "Explain how type annotations help prevent bugs.", ] From f83812f18a8e07eab7c8aba143522865376e43e8 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 30 Sep 2026 16:01:36 +0100 Subject: [PATCH 20/33] update task text --- common-content/en/module/decomposition/dataclasses/index.md | 6 ++---- common-content/en/module/decomposition/enums/index.md | 2 +- common-content/en/module/decomposition/generics/index.md | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/common-content/en/module/decomposition/dataclasses/index.md b/common-content/en/module/decomposition/dataclasses/index.md index d7ad233f0..5b08e2028 100644 --- a/common-content/en/module/decomposition/dataclasses/index.md +++ b/common-content/en/module/decomposition/dataclasses/index.md @@ -77,11 +77,9 @@ Other languages have a similar idea of a value type, and tools to help make them Work in file `10-implement.py` for this task. -Copy what you have done so far from task 8 into this file. - -Convert your existing `Person` class from task 8 into a value type using `@datatype` so you can print the class (and see it's type and properties) and compare class instances that are identical. Make sure your `is_adult` method and `drivers_license_check` free function both work as normal. +Convert the above `Person` class into a value type using `@dataclass` so you can print the class (and see it's type and properties) and compare class instances that are identical. Make sure your `is_adult` method and `drivers_license_check` free function both work. Make a new method on your Person class - `greet` which should return `"Hello !"` when used. -Read the [`@datatype` documentation](https://docs.python.org/3/library/dataclasses.html). Explain what `frozen=True` does to the class? What other options could you play around with and explore? +Read the [`@dataclass` documentation](https://docs.python.org/3/library/dataclasses.html). Explain what `frozen=True` would do to the class? What other options could you play around with and explore? {{}} diff --git a/common-content/en/module/decomposition/enums/index.md b/common-content/en/module/decomposition/enums/index.md index 2ecf318cd..7eaf3bc0a 100644 --- a/common-content/en/module/decomposition/enums/index.md +++ b/common-content/en/module/decomposition/enums/index.md @@ -60,7 +60,7 @@ Refactor the code to use enums for operating systems. Check with mypy and test it to ensure the program still works correctly. -Replace the list of existing people with [the `input` function](https://docs.python.org/3/library/functions.html#input) to read a person's name, age, and preferred operating system. +Use [the `input` function](https://docs.python.org/3/library/functions.html#input) to read a person's name, age, and preferred operating system, then add them to the list of people Make sure your implementation has a good user experience, and properly validates the inputs, mapping an OS to one of the enum values. diff --git a/common-content/en/module/decomposition/generics/index.md b/common-content/en/module/decomposition/generics/index.md index 2b9bcc146..5908574ad 100644 --- a/common-content/en/module/decomposition/generics/index.md +++ b/common-content/en/module/decomposition/generics/index.md @@ -115,13 +115,13 @@ It also means instead of having to create a new method to print out every single {{}} **Task 12** -We are going to improve the printing in the above code, you can find a copy in `12-fix.py`. +We are going to improve the printing of the code in the file in `12-fix.py`. Experiment with mypy and make sure that the family tree only takes `Person` types and the species tree only takes `Animal` types. Currently the `Tree.print_tree()` method doesn't look very pretty. -Change only the Animal and Person classes to allow the `Tree.print_tree()` method to display an output that looks like this: +Update the code, using __str__ methods in `Animal` and `Person` to allow the `Tree.print_tree()` method to display an output that looks like this: ``` Imran (30 years old) From 19a988d68d08fba0e54960fd8a88ec2e1b5ac89c Mon Sep 17 00:00:00 2001 From: LonMcGregor <3817332+LonMcGregor@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:40:57 +0100 Subject: [PATCH 21/33] Update common-content/en/module/decomposition/classes-and-objects/index.md Co-authored-by: Daniel Wagner-Hall --- .../en/module/decomposition/classes-and-objects/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common-content/en/module/decomposition/classes-and-objects/index.md b/common-content/en/module/decomposition/classes-and-objects/index.md index 079b764da..aae9eb6f9 100644 --- a/common-content/en/module/decomposition/classes-and-objects/index.md +++ b/common-content/en/module/decomposition/classes-and-objects/index.md @@ -81,8 +81,8 @@ The method called `__init__` is called a constructor - it is what is called when {{}} From 4bb72daedf7558f406842a525b6cba438b0de72a Mon Sep 17 00:00:00 2001 From: l Date: Mon, 5 Oct 2026 10:41:37 +0100 Subject: [PATCH 22/33] review types intro --- .../decomposition/type-checking-with-mypy/index.md | 7 ++++++- .../en/module/decomposition/why-types/index.md | 9 ++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/common-content/en/module/decomposition/type-checking-with-mypy/index.md b/common-content/en/module/decomposition/type-checking-with-mypy/index.md index 8eec46f2e..d2c9b34b6 100644 --- a/common-content/en/module/decomposition/type-checking-with-mypy/index.md +++ b/common-content/en/module/decomposition/type-checking-with-mypy/index.md @@ -2,6 +2,7 @@ title = "Type checking with mypy" time = 60 objectives = [ + "Explain what a type annotation is.", "Run mypy to detect type errors in Python.", "Annotate function signatures with types in Python.", ] @@ -22,7 +23,11 @@ Other languages, like JavaScript and Python, _don't require_ this but they have Some very low level machine languages like assembly don't have any typing at all. -Languages with optional type checking perform good checks when you add this type information. If you don't add type annotations in your code, they will perform fewer checks. Sometimes they will infer the correct types based on what you _have_ annotated. Other times they will just ignore code with no annotations and not give you errors about it even if it's wrong. +Languages with optional type checking perform good checks when you add this type information through _annotations_. Annotations are a way of hinting at a type, for example that a variable contains a certain type, or that a function returns a certain type. + +For example, imagine we had a variable `price` and we want to annotate its type. It could be a formatted string like `"£3.50"`. A float like `3.5`. If we _annotated_ it as `price : int` it would be good hint that it should only contains whole numbers, so lets the programmer and the language know the code expects a value like `350`. + +If you don't add type annotations in your code, the language will perform fewer checks. Sometimes they will infer the correct types based on what you _have_ annotated. Other times they will just ignore code with no annotations and not give you errors about it even if it's wrong. ### Trying out Mypy Mypy is a tool which enables type checking in Python code. It works best if you can build up a habit of integrating it into your workflow. diff --git a/common-content/en/module/decomposition/why-types/index.md b/common-content/en/module/decomposition/why-types/index.md index 6a7549c06..8308204b8 100644 --- a/common-content/en/module/decomposition/why-types/index.md +++ b/common-content/en/module/decomposition/why-types/index.md @@ -3,8 +3,7 @@ title = "Why we use types" time = 30 objectives = [ "Explain what a type is.", - "Explain how type annotations help understand a function's expectations.", - "Explain how type annotations help prevent bugs.", + "Demonstrate why knowing types can help prevent bugs.", ] [build] @@ -16,7 +15,7 @@ objectives = [ In real life, as well as programming, there are some impossible operations. Can you divide seven by yellow? Can you set fire to a sound? These don't make sense. The same is true in programming. -Throughout this sprint we are going to look at some code which you can find in the file `SDC-Tools/sprint-5` directory of [the Module-Tools repository](https://github.com/CodeYourFuture/Module-Tools). +Throughout this sprint we are going to look at some code which you can find in the `types` directory of [the Module-Tools repository](https://github.com/CodeYourFuture/Module-Tools). {{}} **Task 1** @@ -46,7 +45,7 @@ In such a simple program as in `01-predict.py`, it's easy for us to run the prog {{}} **Task 2** -Have a look now at `02-playcomputer.py`. +Have a look now at `02-playcomputer.js`. Read through this file and predict what it does. @@ -57,7 +56,7 @@ Leave a comment on any lines if you spot any errors, offering an explanation of How many errors did you find in your testing? There is one big bug here which doesn't always show. `response.body` is a _stream_ not a _string_. So if a user ever tries to fetch a URL which returns a non-200 status code, our program will crash: ```console -% node fetch.js +% node 02-playcomputer.js What URL should we fetch? > http://www.google.com/beepboop file:///Users/dwh/tmp/jsplay/fetch.js:12 From a92f4e646fca317fa1a0a5063d209c3d87ba30f1 Mon Sep 17 00:00:00 2001 From: l Date: Mon, 5 Oct 2026 10:45:06 +0100 Subject: [PATCH 23/33] review example code quality --- common-content/en/module/decomposition/methods/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common-content/en/module/decomposition/methods/index.md b/common-content/en/module/decomposition/methods/index.md index 8e3d39d6b..8fcb73824 100644 --- a/common-content/en/module/decomposition/methods/index.md +++ b/common-content/en/module/decomposition/methods/index.md @@ -60,7 +60,7 @@ Consider this free function called `drivers_license_check` which uses the Person ```python def drivers_license_check(person: Person): - if person.is_adult() == True: + if person.is_adult(): return 'Valid drivers license' return 'This person is underage!' From c377f978b685f981be4005111bf529ce84fdc711 Mon Sep 17 00:00:00 2001 From: LonMcGregor <3817332+LonMcGregor@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:45:51 +0100 Subject: [PATCH 24/33] Update common-content/en/module/decomposition/methods/index.md Co-authored-by: Daniel Wagner-Hall --- common-content/en/module/decomposition/methods/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common-content/en/module/decomposition/methods/index.md b/common-content/en/module/decomposition/methods/index.md index 8fcb73824..b125f7c9c 100644 --- a/common-content/en/module/decomposition/methods/index.md +++ b/common-content/en/module/decomposition/methods/index.md @@ -76,7 +76,7 @@ Work inside the `08-implement.py` file for this task. Add an `is_adult` method into the class, and make sure your code gives the expected output. -Change the `Person` class to take a date of birth (using [the standard library's `datetime.date` class](https://docs.python.org/3/library/datetime.html#datetime.date)) and store the `date of birth` instead of `age`. +Change the `Person` class to take a date of birth (using [the standard library's `datetime.date` class](https://docs.python.org/3/library/datetime.html#datetime.date)) and store the `date_of_birth` instead of `age`. **Try to run your code now** and observe how this change breaks your code. What kind of error do you get? Is it helpful in identifying where your next change needs to be? From 834020fc4ae7c2c072641719d8979fd609f7b4a3 Mon Sep 17 00:00:00 2001 From: LonMcGregor <3817332+LonMcGregor@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:46:04 +0100 Subject: [PATCH 25/33] Update common-content/en/module/decomposition/enums/index.md Co-authored-by: Daniel Wagner-Hall --- common-content/en/module/decomposition/enums/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common-content/en/module/decomposition/enums/index.md b/common-content/en/module/decomposition/enums/index.md index 7eaf3bc0a..e2a0d9311 100644 --- a/common-content/en/module/decomposition/enums/index.md +++ b/common-content/en/module/decomposition/enums/index.md @@ -44,7 +44,7 @@ This defines a new type called `OperatingSystem` which has three possible values > There are lots of ways different programming deal with the concept enums. > Some, like JavaScript, have no built-in way to use enums. > Python treats enums as a special kind of class mapping definitions to a value. -> Others, like Rust, have more advanced typing systems that can treat enumerations as standalone types. +> Others, like Rust, have more advanced typing systems that can treat enums as standalone types. We know that when we save data, transfer it across a network, or take user input, everything comes in as bytes. A typical pattern in software is to accept a string in the user input, and convert it to an enum before passing it into any other function. If the string wasn't a valid operating system we know about, we will reject it and give an error when we first accept it. All of our other functions can take an `OperatingSystem` as a parameter, and know that any value it's given _must_ be a valid operating system. This restricts where we need to worry about incorrect input - once we've checked that the string was correct one time, the rest of our code doesn't have to worry about incorrect strings. From 7492121eaffef700c9bc4017e42034a883fc8aef Mon Sep 17 00:00:00 2001 From: LonMcGregor <3817332+LonMcGregor@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:46:17 +0100 Subject: [PATCH 26/33] Update common-content/en/module/decomposition/inheritance/index.md Co-authored-by: Daniel Wagner-Hall --- common-content/en/module/decomposition/inheritance/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common-content/en/module/decomposition/inheritance/index.md b/common-content/en/module/decomposition/inheritance/index.md index 0376f8841..a829ed337 100644 --- a/common-content/en/module/decomposition/inheritance/index.md +++ b/common-content/en/module/decomposition/inheritance/index.md @@ -14,7 +14,7 @@ objectives = [ render = "never" +++ -In this prep we have seen how add methods to classes to encapsulate functionality. We have seen how to use generics to force classes to work with certain types. Keeping code reusability and maintainability in mind, what if we wanted to add a new class that did mostly the same as an existing class, but with some slight changes? +In this prep we have seen how to add methods to classes to encapsulate functionality. We have seen how to use generics to force classes to work with certain types. Keeping code reusability and maintainability in mind, what if we wanted to add a new class that did mostly the same as an existing class, but with some slight changes? Classes can _extend_ other classes to share most of their functionality but add or replace some of it. A class that carries over something from another class is called _inheritance_. From 6f81dbc3bc5d3831fdfcc0627394aa2c36b050a3 Mon Sep 17 00:00:00 2001 From: LonMcGregor <3817332+LonMcGregor@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:46:36 +0100 Subject: [PATCH 27/33] Update common-content/en/module/decomposition/inheritance/index.md Co-authored-by: Daniel Wagner-Hall --- common-content/en/module/decomposition/inheritance/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common-content/en/module/decomposition/inheritance/index.md b/common-content/en/module/decomposition/inheritance/index.md index a829ed337..f9e7059a4 100644 --- a/common-content/en/module/decomposition/inheritance/index.md +++ b/common-content/en/module/decomposition/inheritance/index.md @@ -16,7 +16,7 @@ objectives = [ In this prep we have seen how to add methods to classes to encapsulate functionality. We have seen how to use generics to force classes to work with certain types. Keeping code reusability and maintainability in mind, what if we wanted to add a new class that did mostly the same as an existing class, but with some slight changes? -Classes can _extend_ other classes to share most of their functionality but add or replace some of it. A class that carries over something from another class is called _inheritance_. +Classes can _extend_ other classes to share most of their functionality but add or replace some of it. A class carrying over something from another class is called _inheritance_. Read the code in file `15-analyse.py`. From aaf08a9393eb38c98aa2db58d472fe245174c2c5 Mon Sep 17 00:00:00 2001 From: LonMcGregor <3817332+LonMcGregor@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:46:58 +0100 Subject: [PATCH 28/33] Update common-content/en/module/decomposition/generics/index.md Co-authored-by: Daniel Wagner-Hall --- common-content/en/module/decomposition/generics/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common-content/en/module/decomposition/generics/index.md b/common-content/en/module/decomposition/generics/index.md index 5908574ad..346d1edf1 100644 --- a/common-content/en/module/decomposition/generics/index.md +++ b/common-content/en/module/decomposition/generics/index.md @@ -54,7 +54,7 @@ Now that we've told mypy `FamilyTree.members` is a list of type `Person`, it can > [!NOTE] > -> I you want to _recursively_ reference a type within a class, we need to quote it for mypy to recognise it. +> If you want to _recursively_ reference a type within a class, we need to quote it for mypy to recognise it. > So for example, if we wanted a `Person` object to include a list of children, we would write it as `List["Person"]`. > > It's kind of annoying, but don't worry about it too much. From 33194ebab9880c7c1c574ca27646cc93cf2c0c90 Mon Sep 17 00:00:00 2001 From: LonMcGregor <3817332+LonMcGregor@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:47:07 +0100 Subject: [PATCH 29/33] Update common-content/en/module/decomposition/inheritance/index.md Co-authored-by: Daniel Wagner-Hall --- common-content/en/module/decomposition/inheritance/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common-content/en/module/decomposition/inheritance/index.md b/common-content/en/module/decomposition/inheritance/index.md index f9e7059a4..d91e095d9 100644 --- a/common-content/en/module/decomposition/inheritance/index.md +++ b/common-content/en/module/decomposition/inheritance/index.md @@ -44,7 +44,7 @@ Q1: `SortedImmutableNumberList` sorts the numbers in advance, and the method imp Q2: `ImmutableNumberList` doesn't need to sort the numbers immediately on creation. If you only intended to use `first` and `last`, it may be faster. Of course, it all depends on which functionality you think you will need. -You will learn more about these efficiency concepts in the upcoming complexity module. +You will learn more about these efficiency concepts in the Complexity module. {{}} From fc71d54b05729ac8ef6590ef9a1e06fcbe0a3e6b Mon Sep 17 00:00:00 2001 From: LonMcGregor <3817332+LonMcGregor@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:47:31 +0100 Subject: [PATCH 30/33] Update common-content/en/module/decomposition/inheritance/index.md Co-authored-by: Daniel Wagner-Hall --- common-content/en/module/decomposition/inheritance/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/common-content/en/module/decomposition/inheritance/index.md b/common-content/en/module/decomposition/inheritance/index.md index d91e095d9..daeb7ec9d 100644 --- a/common-content/en/module/decomposition/inheritance/index.md +++ b/common-content/en/module/decomposition/inheritance/index.md @@ -40,6 +40,7 @@ Q2: If you know in advance you will be initialising many of them repeatedly, whi
Expand for some answers after you've listed your own. + Q1: `SortedImmutableNumberList` sorts the numbers in advance, and the method implementation for largest item only needs to look at the final item of the sorted list. This means accessing it is faster. Q2: `ImmutableNumberList` doesn't need to sort the numbers immediately on creation. If you only intended to use `first` and `last`, it may be faster. From d2b3543ac43dd4578db64f5bbcd67d4cc3c59862 Mon Sep 17 00:00:00 2001 From: LonMcGregor <3817332+LonMcGregor@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:48:21 +0100 Subject: [PATCH 31/33] Update common-content/en/module/decomposition/enums/index.md Co-authored-by: Daniel Wagner-Hall --- common-content/en/module/decomposition/enums/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common-content/en/module/decomposition/enums/index.md b/common-content/en/module/decomposition/enums/index.md index e2a0d9311..060317180 100644 --- a/common-content/en/module/decomposition/enums/index.md +++ b/common-content/en/module/decomposition/enums/index.md @@ -24,7 +24,7 @@ Some common problems with strings: Did you spot in the bug in task 12? The laptop with id 3 was never put in anyone's preferred list, because its operating system was spelled `Ubuntu` not `ubuntu`. -We can use enums to represent that one some values are allowed, and make sure we're always using the same ones. This is similar to how in HTML we can use a `` to restrict what a user can enter into a form. +We can use enums to represent that only some values are allowed, and make sure we're always using the same ones. This is similar to how in HTML we can use a `` to restrict what a user can enter into a form. In Python, we can define an enum as a new type. This is like `bool` - `bool` is a type which has two possible values (`True` and `False`). We can make enums that have any number of possible values, and we can choose the values' names. From 2a363b978e2190b29664b53741d35d75dc13b734 Mon Sep 17 00:00:00 2001 From: l Date: Mon, 5 Oct 2026 16:35:42 +0100 Subject: [PATCH 32/33] review encapsulation summary --- .../decomposition/encapsulation/index.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/common-content/en/module/decomposition/encapsulation/index.md b/common-content/en/module/decomposition/encapsulation/index.md index 98f525981..79dba89a5 100644 --- a/common-content/en/module/decomposition/encapsulation/index.md +++ b/common-content/en/module/decomposition/encapsulation/index.md @@ -92,3 +92,29 @@ Think of some examples and in your own words write down some benefits and trade- Working in file `09-encapsulation.py`, make the `name` field private, and add a `get_name()` method to allow read-only access. {{}} + +### Why encapsulate? + +In your career you will rarely be building code used only once. +It is likely the code you write will sit alongside code written by others as part of a large long-lived codebase. +Classes and encapsulation are really important techniques as you move towards thinking about how others will use your code, and how you plan to make your code maintainable and reusable for future use. + +Classes with encapsulation clearly define the outward-facing interface of what you are building. +Think about the documentation you may have read for well-defined APIs like [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or [argparse](https://docs.python.org/3/library/argparse.html). +You don't need to know how they work internally to make use of them, and the methods and their parameters are clearly stated. +If `argparse` is updated, e.g. to make it more efficient, your code won't break as the public interface won't change. +If `fetch` is changed, e.g. adding a new parameter, type checking will immediately highlight everywhere you need to update your code. + +Encapsulation also makes it easy to swap different implementations. +Imagine you started a big project with a python `dict` but later on needed to change it to an [OrderedDict](https://docs.python.org/3/library/collections.html#collections.OrderedDict). +The interfaces are almost exactly the same, so you wouldn't need to change any of the method invocations, making the change much easier and safer. + +Encapsulation also helps with testing. +Only the public interface, methods and properties, need to be tested. +You can write the test before you start using test-driven development, defining the public interface and behaviour. +Then you can focus on the implementation inside, and when the test passes you know your class works. +Testing a single class with a well defined interface is much easier than needing to test lots of interconnected separate free functions. + +Until now you have been solving small coding challenges with the aim of solving the specific task. +From now on you will start to think more about how you can build a solution that will adapt well to future changes. +Well defined classes that encapsulate your implementations will be a big help. From c477a87a10945c39a0768c4204bdacd517cb12cb Mon Sep 17 00:00:00 2001 From: l Date: Tue, 6 Oct 2026 14:48:26 +0100 Subject: [PATCH 33/33] review generics --- .../en/module/decomposition/generics/index.md | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/common-content/en/module/decomposition/generics/index.md b/common-content/en/module/decomposition/generics/index.md index 346d1edf1..91631ee60 100644 --- a/common-content/en/module/decomposition/generics/index.md +++ b/common-content/en/module/decomposition/generics/index.md @@ -5,6 +5,7 @@ objectives = [ "Define a generic type.", "Explain why generics are useful.", "Use a generic in a type annotation.", + "Create your own generics.", ] [build] @@ -13,12 +14,12 @@ objectives = [ render = "never" +++ -### A problem type checking can't spot - -Sometimes we want to reason about more complicated type relationships than "this field is a string". Lists and dicts are examples of this. We may want to reason that every value in a list is a string. +### Limitations of Type Annotations +Sometimes we want to reason about more complicated type relationships than "this field is a string". Lists and dictionaries are examples of this. We may want to reason that every value in a list is a certain type. We could model a family as being a list of members all of type `Person`, for instance. {{}} **Task 11** + Have a look at the code in `11-predict.py` There is a bug in this code. Can you spot it? @@ -39,12 +40,10 @@ That's what's happening in task 11. `FamilyTree.members` is a `list`, but mypy d We can use {{}}A list could store numbers, or strings. We use generic types to say which type a particular instance of a list stores. Even though we can have a list of strings, and a list of numbers, the code for finding the first element is the same. But knowing that a list _only_ contains strings is useful.{{}} to tell mypy what type of thing is in the list. We could add an import and modify the `FamilyTree` class from task 11 as follows: ```python -from typing import List - @dataclass(frozen=True) class FamilyTree: parent: Person - members: List[Person] + members: list[Person] ``` Try updating your code for task 11 with this change and see if mypy spots the problem @@ -60,7 +59,39 @@ Now that we've told mypy `FamilyTree.members` is a list of type `Person`, it can > It's kind of annoying, but don't worry about it too much. -### Writing our own classes that use generics + +### Generic Functions + +You have seen how we can use generics in type annotations. But what if we wanted to make the code we write support generics as well? + +Think about a common task you may have done, getting the last element from a list. You can do this using indexing: `[-1]`. +It should be pretty simple to turn this into a free function, but how do we annotate the types? +This function needs to work with any typed list, not just lists of integers or strings. +It would be too much work to write a separate function for every possible type of list. + +Instead, we write a _generic function_. +To make a generic function we pick a symbol, like the letter `T`, to represent a given type. +After the function name, we add {{}}Just like you can define parameters for the values passed to a function, you can define parameters that describe the generic types used within a function. These come before the main parameters and use square brackets: `[T]`. Some languages use angle brackets instead: ``.{{}} the function using `[T]`. +From then on, everywhere in the function that has a `T` becomes the given type. + +Here is an example of a generic function: + +```py +def get_last[T](my_list: list[T]) -> T: + return -1 + +get_last([0,1,2,3,4,5]) +get_last(["a","b","c"]) +``` + +This function contains a bug. +Mypy can find the error that this function always returns an integer, rather than returning the same type as is held in the list. +In this way we can write a generic free function that can be used with any type, and which works well with mypy. + + +### Writing a Generic Class + +We're starting to move beyond writing free functions, though. How does this work with classes? The kind of relationship structure we created with families and members, a {{}}A list can store a linear array of values. A tree stores values in a heirarchy, like a family tree.{{}}, is common across many types of data, for example how species of animal are related to each other, or how a dictionary might store words. @@ -70,7 +101,6 @@ Look at the following code: ```python from dataclasses import dataclass -from typing import List @dataclass(frozen=True) class Animal: @@ -85,7 +115,7 @@ class Person: @dataclass(frozen=True) class Tree[T]: parent: T - children: List[T] + children: list[T] def print_tree(self): print(self.parent) @@ -106,9 +136,9 @@ family_tree.print_tree() species_tree.print_tree() ``` -The Tree here has a special type annotation given by `T`. This is a generic, telling python that whatever type is given, every reference to `T` within the class becomes that type. +The Tree here has the type paramter `T`. Just like with a generic function, this tells python that every reference to `T` within the class becomes a given type. -Observe that we then create two different trees: a `Tree` and a `Tree`. In these trees, the parent and list of children must contain `Person` and `Animal` types respectively. +See we then create two different trees: a `Tree[Person]` and a `Tree[Animal]`. In these trees, the parent and list of children must contain `Person` and `Animal` types respectively. It also means instead of having to create a new method to print out every single tree type, we can create a single method - `Tree.print_tree()`. @@ -121,12 +151,12 @@ Experiment with mypy and make sure that the family tree only takes `Person` type Currently the `Tree.print_tree()` method doesn't look very pretty. -Update the code, using __str__ methods in `Animal` and `Person` to allow the `Tree.print_tree()` method to display an output that looks like this: +Update the code, adding `__str__()` methods in `Animal` and `Person` to allow the `Tree.print_tree()` method to display an output that looks like this: -``` +```text Imran (30 years old) - Fatma (4 years old) -- Aisha (6 years old)) +- Aisha (6 years old) Mammals (Variable size) - Cat (Small size) - Dog (Medium size)