📝 Update all docs references to Optional to use the new syntax in Python 3.10, e.g. int | None (#1351)

This commit is contained in:
Sebastián Ramírez
2025-04-27 20:53:37 +02:00
committed by GitHub
parent 0e5e19773c
commit 61523864f1
15 changed files with 52 additions and 66 deletions

View File

@@ -4,7 +4,7 @@ In the previous chapter, we saw how to add rows to the database using **SQLModel
Now let's talk a bit about why the `id` field **can't be `NULL`** on the database because it's a **primary key**, and we declare it using `Field(primary_key=True)`.
But the same `id` field actually **can be `None`** in the Python code, so we declare the type with `int | None (or Optional[int])`, and set the default value to `Field(default=None)`:
But the same `id` field actually **can be `None`** in the Python code, so we declare the type with `int | None`, and set the default value to `Field(default=None)`:
{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[4:8] hl[5] *}
@@ -18,15 +18,15 @@ When we create a new `Hero` instance, we don't set the `id`:
{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[21:24] hl[21:24] *}
### How `Optional` Helps
### How `int | None` Helps
Because we don't set the `id`, it takes the Python's default value of `None` that we set in `Field(default=None)`.
This is the only reason why we define it with `Optional` and with a default value of `None`.
This is the only reason why we define it with `int | None` and with a default value of `None`.
Because at this point in the code, **before interacting with the database**, the Python value could actually be `None`.
If we assumed that the `id` was *always* an `int` and added the type annotation without `Optional`, we could end up writing broken code, like:
If we assumed that the `id` was *always* an `int` and added the type annotation without `int | None`, we could end up writing broken code, like:
```Python
next_hero_id = hero_1.id + 1
@@ -38,7 +38,7 @@ If we ran this code before saving the hero to the database and the `hero_1.id` w
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
```
But by declaring it with `Optional[int]`, the editor will help us to avoid writing broken code by showing us a warning telling us that the code could be invalid if `hero_1.id` is `None`. 🔍
But by declaring it with `int | None`, the editor will help us to avoid writing broken code by showing us a warning telling us that the code could be invalid if `hero_1.id` is `None`. 🔍
## Print the Default `id` Values

View File

@@ -141,7 +141,7 @@ WHERE team.id = ?
INFO Engine [cached since 0.001795s ago] (1,)
```
There's something else to note. We marked `team_id` as `Optional[int]`, meaning that this could be `NULL` on the database (and `None` in Python).
There's something else to note. We marked `team_id` as `int | None`, meaning that this could be `NULL` on the database (and `None` in Python).
That means that a hero doesn't have to have a team. And in this case, **Spider-Boy** doesn't have one.

View File

@@ -67,11 +67,9 @@ And the type of each of them will also be the type of table column:
Let's now see with more detail these field/column declarations.
### Optional Fields, Nullable Columns
### `None` Fields, Nullable Columns
Let's start with `age`, notice that it has a type of `int | None (or Optional[int])`.
And we import that `Optional` from the `typing` standard module.
Let's start with `age`, notice that it has a type of `int | None`.
That is the standard way to declare that something "could be an `int` or `None`" in Python.
@@ -81,21 +79,23 @@ And we also set the default value of `age` to `None`.
/// tip
We also define `id` with `Optional`. But we will talk about `id` below.
We also define `id` with `int | None`. But we will talk about `id` below.
///
This way, we tell **SQLModel** that `age` is not required when validating data and that it has a default value of `None`.
Because the type is `int | None`:
And we also tell it that, in the SQL database, the default value of `age` is `NULL` (the SQL equivalent to Python's `None`).
* When validating data, `None` will be an allowed value for `age`.
* In the database, the column for `age` will be allowed to have `NULL` (the SQL equivalent to Python's `None`).
So, this column is "nullable" (can be set to `NULL`).
And because there's a default value `= None`:
/// info
* When validating data, this `age` field won't be required, it will be `None` by default.
* When saving to the database, the `age` column will have a `NULL` value by default.
In terms of **Pydantic**, `age` is an **optional field**.
/// tip
In terms of **SQLAlchemy**, `age` is a **nullable column**.
The default value could have been something else, like `= 42`.
///
@@ -111,7 +111,7 @@ To do that, we use the special `Field` function from `sqlmodel` and set the argu
That way, we tell **SQLModel** that this `id` field/column is the primary key of the table.
But inside the SQL database, it is **always required** and can't be `NULL`. Why should we declare it with `Optional`?
But inside the SQL database, it is **always required** and can't be `NULL`. Why should we declare it with `int | None`?
The `id` will be required in the database, but it will be *generated by the database*, not by our code.
@@ -128,7 +128,7 @@ somehow_save_in_db(my_hero)
do_something(my_hero.id) # Now my_hero.id has a value generated in DB 🎉
```
So, because in *our code* (not in the database) the value of `id` *could be* `None`, we use `Optional`. This way **the editor will be able to help us**, for example, if we try to access the `id` of an object that we haven't saved in the database yet and would still be `None`.
So, because in *our code* (not in the database) the value of `id` *could be* `None`, we use `int | None`. This way **the editor will be able to help us**, for example, if we try to access the `id` of an object that we haven't saved in the database yet and would still be `None`.
<img class="shadow" src="/img/create-db-and-table/inline-errors01.png">

View File

@@ -16,7 +16,7 @@ For input, we have:
If we pay attention, it shows that the client *could* send an `id` in the JSON body of the request.
This means that the client could try to use the same ID that already exists in the database for another hero.
This means that the client could try to use the same ID that already exists in the database to create another hero.
That's not what we want.
@@ -51,7 +51,7 @@ The `age` is optional, we don't have to return it, or it could be `None` (or `nu
Here's the weird thing, the `id` currently seems also "optional". 🤔
This is because in our **SQLModel** class we declare the `id` with `Optional[int]`, because it could be `None` in memory until we save it in the database and we finally get the actual ID.
This is because in our **SQLModel** class we declare the `id` with a default value of `= None`, because it could be `None` in memory until we save it in the database and we finally get the actual ID.
But in the responses, we always send a model from the database, so it **always has an ID**. So the `id` in the responses can be declared as required.
@@ -71,7 +71,7 @@ And in most of the cases, the developer of the client for that API **will also b
### So Why is it Important to Have Required IDs
Now, what's the matter with having one **`id` field marked as "optional"** in a response when in reality it is always required?
Now, what's the matter with having one **`id` field marked as "optional"** in a response when in reality it is always available (required)?
For example, **automatically generated clients** in other languages (or also in Python) would have some declaration that this field `id` is optional.
@@ -98,7 +98,7 @@ But we also want to have a `HeroCreate` for the data we want to receive when **c
* `secret_name`, required
* `age`, optional
And we want to have a `HeroPublic` with the `id` field, but this time annotated with `id: int`, instead of `id: Optional[int]`, to make it clear that it is required in responses **read** from the clients:
And we want to have a `HeroPublic` with the `id` field, but this time with a type of `id: int`, instead of `id: int | None`, to make it clear that it will always have an `int` in responses **read** from the clients:
* `id`, required
* `name`, required
@@ -225,7 +225,7 @@ Let's start with the only **table model**, the `Hero`:
Notice that `Hero` now doesn't inherit from `SQLModel`, but from `HeroBase`.
And now we only declare one single field directly, the `id`, that here is `Optional[int]`, and is a `primary_key`.
And now we only declare one single field directly, the `id`, that here is `int | None`, and is a `primary_key`.
And even though we don't declare the other fields **explicitly**, because they are inherited, they are also part of this `Hero` model.

View File

@@ -8,15 +8,15 @@ We want clients to be able to update the `name`, the `secret_name`, and the `age
But we don't want them to have to include all the data again just to **update a single field**.
So, we need to have all those fields **marked as optional**.
So, we need to make all those fields **optional**.
And because the `HeroBase` has some of them as *required* and not optional, we will need to **create a new model**.
And because the `HeroBase` has some of them *required* (without a default value), we will need to **create a new model**.
/// tip
Here is one of those cases where it probably makes sense to use an **independent model** instead of trying to come up with a complex tree of models inheriting from each other.
Because each field is **actually different** (we just change it to `Optional`, but that's already making it different), it makes sense to have them in their own model.
Because each field is **actually different** (we just set a default value of `None`, but that's already making it different), it makes sense to have them in their own model.
///

View File

@@ -46,7 +46,7 @@ We **removed** the previous `team_id` field (column) because now the relationshi
The relationship attribute is now named **`teams`** instead of `team`, as now we support multiple teams.
It is no longer an `Optional[Team]` but a list of teams, annotated as **`list[Team]`**.
It no longer has a type of `Team | None` but a list of teams, the type is now declared as **`list[Team]`**.
We are using the **`Relationship()`** here too.

View File

@@ -68,15 +68,15 @@ if hero.team:
print(hero.team.name)
```
## Optional Relationship Attributes
## Relationship Attributes or `None`
Notice that in the `Hero` class, the type annotation for `team` is `Optional[Team]`.
Notice that in the `Hero` class, the type annotation for `team` is `Team | None`.
This means that this attribute could be `None`, or it could be a full `Team` object.
This is because the related **`team_id` could also be `None`** (or `NULL` in the database).
If it was required for a `Hero` instance to belong to a `Team`, then the `team_id` would be `int` instead of `Optional[int]`, its `Field` would be `Field(foreign_key="team.id")` instead of `Field(default=None, foreign_key="team.id")` and the `team` attribute would be a `Team` instead of `Optional[Team]`.
If it was required for a `Hero` instance to belong to a `Team`, then the `team_id` would be `int` instead of `int | None`, its `Field` would be `Field(foreign_key="team.id")` instead of `Field(default=None, foreign_key="team.id")` and the `team` attribute would be a `Team` instead of `Team | None`.
## Relationship Attributes With Lists

View File

@@ -690,7 +690,7 @@ It would be an error telling you that
> `Hero.age` is potentially `None`, and you cannot compare `None` with `>`
This is because as we are using pure and plain Python annotations for the fields, `age` is indeed annotated as `int | None (or Optional[int])`.
This is because as we are using pure and plain Python annotations for the fields, `age` is indeed annotated as `int | None`.
By using this simple and standard Python type annotations we get the benefit of the extra simplicity and the inline error checks when creating or using instances. ✨