If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you.
placeholder: |
- from typing import Optional
-
from sqlmodel import Field, Session, SQLModel, create_engine
class Hero(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
Then you could create a **SQLModel** model like this:
```Python
-from typing import Optional
-
from sqlmodel import Field, SQLModel
class Hero(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
```
That class `Hero` is a **SQLModel** model, the equivalent of a SQL table in Python code.
You can learn a lot more about **SQLModel** by quickly following the **tutorial**, but if you need a taste right now of how to put all that together and save to the database, you can do this:
-```Python hl_lines="18 21 23-27"
-from typing import Optional
-
+```Python hl_lines="16 19 21-25"
from sqlmodel import Field, Session, SQLModel, create_engine
class Hero(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
Then you could write queries to select from that same database, for example with:
-```Python hl_lines="15-18"
-from typing import Optional
-
+```Python hl_lines="13-17"
from sqlmodel import Field, Session, SQLModel, create_engine, select
class Hero(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
engine = create_engine("sqlite:///database.db")
```Python
class Hero(SQLModel):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
```
* **Relational**: refers to the **SQL Databases**. Remember that they are also called **Relational Databases**, because each of those tables is also called a "**relation**"? That's where the "**Relational**" comes from.
Then you could create a **SQLModel** model like this:
```Python
-from typing import Optional
-
from sqlmodel import Field, SQLModel
class Hero(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
```
That class `Hero` is a **SQLModel** model, the equivalent of a SQL table in Python code.
You can learn a lot more about **SQLModel** by quickly following the **tutorial**, but if you need a taste right now of how to put all that together and save to the database, you can do this:
-```Python hl_lines="18 21 23-27"
-from typing import Optional
-
+```Python hl_lines="16 19 21-25"
from sqlmodel import Field, Session, SQLModel, create_engine
class Hero(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
Then you could write queries to select from that same database, for example with:
-```Python hl_lines="15-18"
-from typing import Optional
-
+```Python hl_lines="13-17"
from sqlmodel import Field, Session, SQLModel, create_engine, select
class Hero(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
+ id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
- age: Optional[int] = None
+ age: int | None = None
engine = create_engine("sqlite:///database.db")
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] *}
{* ./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
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
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.
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.
/// 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`.
///
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.
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">
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.
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.
### 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.
* `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
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.
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.
///
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.
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
> `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. ✨