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..aae9eb6f9 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. +{{}} + +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.{{}}. @@ -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. {{}} diff --git a/common-content/en/module/decomposition/dataclasses/index.md b/common-content/en/module/decomposition/dataclasses/index.md index 2df62c974..5b08e2028 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: @@ -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") @@ -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,22 +48,23 @@ 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 @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,14 @@ 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. -Re-add the `is_adult` method to it. +**Task 10** + +Work in file `10-implement.py` for this task. + +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 [`@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/encapsulation/index.md b/common-content/en/module/decomposition/encapsulation/index.md new file mode 100644 index 000000000..79dba89a5 --- /dev/null +++ b/common-content/en/module/decomposition/encapsulation/index.md @@ -0,0 +1,120 @@ ++++ +title = "Encapsulation" +time = 30 +objectives = [ + "Define encapsulation.", + "Explain how encapsulation can benefit class design.", +] + +[build] + list = "local" + publishResources = false + render = "never" ++++ + + +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. + +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. +{{}} + +### 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. diff --git a/common-content/en/module/decomposition/enums/index.md b/common-content/en/module/decomposition/enums/index.md index b6497186f..060317180 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] @@ -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 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. @@ -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 enums 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 14** +Look at file `14-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. +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 -{{}} -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. {{}} diff --git a/common-content/en/module/decomposition/generics/index.md b/common-content/en/module/decomposition/generics/index.md index 36476e8ba..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,75 +14,157 @@ objectives = [ render = "never" +++ -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. -Consider this code: +{{}} +**Task 11** -```python -from dataclasses import dataclass +Have a look at the code in `11-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 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 +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 @dataclass(frozen=True) -class Person: - name: str - children: list +class FamilyTree: + parent: Person + members: list[Person] +``` + +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. + + +> [!NOTE] +> +> 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. + -fatma = Person(name="Fatma", children=[]) -aisha = Person(name="Aisha", children=[]) -imran = Person(name="Imran", children=[fatma, aisha]) +### Generic Functions -def print_family_tree(person: Person) -> None: - print(person.name) - for child in person.children: - print(f"- {child.name} ({child.age})") +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? -print_family_tree(imran) +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"]) ``` -There is a bug in this code. Can you spot it? +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. -Run your code through mypy. Does mypy spot it? -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.{{}} +### Writing a Generic Class -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. +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. -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. +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. -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: +Look at the following code: -```python {linenos=table} +```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 - children: List["Person"] + 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() +``` -fatma = Person(name="Fatma", children=[]) -aisha = Person(name="Aisha", children=[]) +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. -imran = Person(name="Imran", children=[fatma, aisha]) +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. -def print_family_tree(person: Person) -> None: - print(person.name) - for child in person.children: - print(f"- {child.name} ({child.age})") +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()`. -print_family_tree(imran) -``` +{{}} +**Task 12** -Run this code through mypy. +We are going to improve the printing of the code in the file in `12-fix.py`. -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. +Experiment with mypy and make sure that the family tree only takes `Person` types and the species tree only takes `Animal` types. -> [!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. -> -> It's kind of annoying, but don't worry about it too much. +Currently the `Tree.print_tree()` method doesn't look very pretty. -{{}} -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.) +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) +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. {{}} diff --git a/common-content/en/module/decomposition/inheritance/index.md b/common-content/en/module/decomposition/inheritance/index.md index 54c79baee..daeb7ec9d 100644 --- a/common-content/en/module/decomposition/inheritance/index.md +++ b/common-content/en/module/decomposition/inheritance/index.md @@ -14,134 +14,63 @@ objectives = [ render = "never" +++ -Classes can extend other classes to share most of their functionality but add or replace some of it. - -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. -``` +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 carrying over something from another class is called _inheritance_. + +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. -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 15** -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). +Work in file `15-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 functionality you think you will need. +You will learn more about these efficiency concepts in the 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). - -```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()) -``` +**Task 16** + +Look at file `16-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). {{}} diff --git a/common-content/en/module/decomposition/methods/index.md b/common-content/en/module/decomposition/methods/index.md index dff21489e..b125f7c9c 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.", - "Implement a method on a class.", + "Add a method to a class.", ] [build] @@ -21,7 +21,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. @@ -38,25 +38,47 @@ 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** -Expand for some answers after you've listed your own. +What is the difference between methods and free functions? -* 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. -
+Do some research about the differences. + +Can you give some advantages of methods over functions? + +Write your thoughts down in `07-methods.txt` {{
}} +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(): + 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. + +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`. + +**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? + +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 eccaa863b..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.", ] @@ -12,49 +13,42 @@ objectives = [ render = "never" +++ -Mypy is a tool which enables type checking in Python code. +### 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 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. {{}} 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. {{}} {{}} -Do not run the following code. - -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 +**Task 4**: -def sum_balances(accounts): - total = 0 - for name, pence in accounts.items(): - print(f"{name} had balance {pence}") - total += pence - return total +Have a look at `04-addmypy.py` -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}" +This code contains bugs related to types. They are bugs mypy can catch. -balances = { - "Sima": 700, - "Linn": 545, - "Georg": 831, -} +Read this code to understand what it's trying to do. -open_account("Tobi", 9.13) -open_account("Olya", "£7.13") +Add type annotations to the method parameters and return types of this code. -total_pence = sum_balances(balances) -total_string = format_pence_as_str(total_pence) +Run the code through mypy, and fix all of the bugs that show up. -print(f"The bank accounts total {total_string}") -``` +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 51253f16e..0b40b04b3 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.", ] @@ -12,73 +12,33 @@ 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. - -```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". +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". + {{}} -Try changing the type annotation of `Person.preferred_operating_system` from `str` to `List[str]`. +**Task 13** +A copy of this file is present in `13-refactor.py`. + +Change 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 remove any 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 field 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. -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". +Then, make sure the program works as you'd expect. +{{
}} diff --git a/common-content/en/module/decomposition/why-types/index.md b/common-content/en/module/decomposition/why-types/index.md index bc6ae7868..8308204b8 100644 --- a/common-content/en/module/decomposition/why-types/index.md +++ b/common-content/en/module/decomposition/why-types/index.md @@ -2,8 +2,8 @@ title = "Why we use types" time = 30 objectives = [ - "Explain how type annotations help understand a function's expectations.", - "Explain how type annotations help prevent bugs.", + "Explain what a type is.", + "Demonstrate why knowing types can help prevent bugs.", ] [build] @@ -12,95 +12,51 @@ 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: +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). -```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 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'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`? -## 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). +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.js`. -```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 on any lines if you spot any errors, offering an explanation of the problem. -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 +% node 02-playcomputer.js What URL should we fetch? > http://www.google.com/beepboop file:///Users/dwh/tmp/jsplay/fetch.js:12 @@ -114,45 +70,34 @@ 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. - -## 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". +How easy was it to spot this bug in your testing? -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. +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. -## Support for type checking +### Types -Different languages have different levels of support for checking types. +This is where types come in. -Some languages, like Java, C++, Rust, and Go, _require_ you to write what types you expect function parameters to have. +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". -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. +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. -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 +### 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. -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. 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..493665f26 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 @@ -17,16 +16,19 @@ 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]] name = "Generics" src = "module/decomposition/generics" [[blocks]] -name = "Type-guided refactorings" +name = "Type-guided refactoring" src = "module/decomposition/type-guided-refactorings" [[blocks]] -name = "enums" +name = "Enums" src = "module/decomposition/enums" [[blocks]] name = "Inheritance"