From: Sebastián Ramírez Date: Sun, 27 Apr 2025 18:53:37 +0000 (+0200) Subject: 📝 Update all docs references to `Optional` to use the new syntax in Python 3.10,... X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=61523864f1f4fc60040f8ae369353460bedc7fe3;p=thirdparty%2Ffastapi%2Fsqlmodel.git 📝 Update all docs references to `Optional` to use the new syntax in Python 3.10, e.g. `int | None` (#1351) --- diff --git a/.github/DISCUSSION_TEMPLATE/questions.yml b/.github/DISCUSSION_TEMPLATE/questions.yml index 97902a65..524d5c2d 100644 --- a/.github/DISCUSSION_TEMPLATE/questions.yml +++ b/.github/DISCUSSION_TEMPLATE/questions.yml @@ -64,16 +64,14 @@ body: 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") diff --git a/README.md b/README.md index f8a32a36..712167ff 100644 --- a/README.md +++ b/README.md @@ -105,16 +105,14 @@ And you want it to have this data: 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. @@ -149,17 +147,15 @@ And **inline errors**: 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") @@ -185,17 +181,15 @@ That will save a **SQLite** database with the 3 heroes. 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") diff --git a/docs/db-to-code.md b/docs/db-to-code.md index 3d289d75..53a8d358 100644 --- a/docs/db-to-code.md +++ b/docs/db-to-code.md @@ -252,10 +252,10 @@ For example this class is part of that **Object** Oriented Programming: ```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. diff --git a/docs/img/index/autocompletion01.png b/docs/img/index/autocompletion01.png index 1a47940b..cba2649f 100644 Binary files a/docs/img/index/autocompletion01.png and b/docs/img/index/autocompletion01.png differ diff --git a/docs/img/index/autocompletion02.png b/docs/img/index/autocompletion02.png index effa22ee..5918e1d2 100644 Binary files a/docs/img/index/autocompletion02.png and b/docs/img/index/autocompletion02.png differ diff --git a/docs/img/index/inline-errors01.png b/docs/img/index/inline-errors01.png index f5ef90ed..b689c556 100644 Binary files a/docs/img/index/inline-errors01.png and b/docs/img/index/inline-errors01.png differ diff --git a/docs/index.md b/docs/index.md index 0feec81c..cd48b913 100644 --- a/docs/index.md +++ b/docs/index.md @@ -118,16 +118,14 @@ And you want it to have this data: 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. @@ -162,17 +160,15 @@ And **inline errors**: 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") @@ -198,17 +194,15 @@ That will save a **SQLite** database with the 3 heroes. 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") diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md index 7c7983c3..0e67633d 100644 --- a/docs/tutorial/automatic-id-none-refresh.md +++ b/docs/tutorial/automatic-id-none-refresh.md @@ -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 diff --git a/docs/tutorial/connect/create-connected-rows.md b/docs/tutorial/connect/create-connected-rows.md index e91c351a..b01d20eb 100644 --- a/docs/tutorial/connect/create-connected-rows.md +++ b/docs/tutorial/connect/create-connected-rows.md @@ -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. diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index 288e2bb2..2f2f34c8 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -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`. diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md index 301c958d..1bc04561 100644 --- a/docs/tutorial/fastapi/multiple-models.md +++ b/docs/tutorial/fastapi/multiple-models.md @@ -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. diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index 0aed115d..e3c8ac6a 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -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. /// diff --git a/docs/tutorial/many-to-many/create-models-with-link.md b/docs/tutorial/many-to-many/create-models-with-link.md index 36a0e10e..587fa436 100644 --- a/docs/tutorial/many-to-many/create-models-with-link.md +++ b/docs/tutorial/many-to-many/create-models-with-link.md @@ -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. diff --git a/docs/tutorial/relationship-attributes/define-relationships-attributes.md b/docs/tutorial/relationship-attributes/define-relationships-attributes.md index 2646082c..c5307d36 100644 --- a/docs/tutorial/relationship-attributes/define-relationships-attributes.md +++ b/docs/tutorial/relationship-attributes/define-relationships-attributes.md @@ -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 diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md index 7e0fe97c..b6d08e72 100644 --- a/docs/tutorial/where.md +++ b/docs/tutorial/where.md @@ -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. ✨