Skip to content

Commit b76334f

Browse files
authored
📝 Settings using lru_cache (fastapi#1214)
* ✨ Update settings examples to use lru_cache * 📝 Update docs for Settings, using @lru_cache * 🎨 Update lru_cache colors to show difference in stored values
1 parent 14b467d commit b76334f

File tree

5 files changed

+109
-29
lines changed

5 files changed

+109
-29
lines changed

docs/en/docs/advanced/settings.md

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ Hello World from Python
119119

120120
These environment variables can only handle text strings, as they are external to Python and have to be compatible with other programs and the rest of the system (and even with different operating systems, as Linux, Windows, macOS).
121121

122-
That means that any value read in Python from an environment variable will be a `str`, and any conversion to a different type or validation has be done in code.
122+
That means that any value read in Python from an environment variable will be a `str`, and any conversion to a different type or validation has to be done in code.
123123

124124
## Pydantic `Settings`
125125

@@ -137,6 +137,9 @@ You can use all the same validation features and tools you use for Pydantic mode
137137
{!../../../docs_src/settings/tutorial001.py!}
138138
```
139139

140+
!!! tip
141+
If you want something quick to copy and paste, don't use this example, use the last one below.
142+
140143
Then, when you create an instance of that `Settings` class (in this case, in the `settings` object), Pydantic will read the environment variables in a case-insensitive way, so, an upper-case variable `APP_NAME` will still be read for the attribute `app_name`.
141144

142145
Next it will convert and validate the data. So, when you use that `settings` object, you will have data of the types you declared (e.g. `items_per_user` will be an `int`).
@@ -151,7 +154,7 @@ Then you can use the new `settings` object in your application:
151154

152155
### Run the server
153156

154-
Then you would run the server passing the configurations as environment variables, for example you could set an `ADMIN_EMAIL` and `APP_NAME` with:
157+
Next, you would run the server passing the configurations as environment variables, for example you could set an `ADMIN_EMAIL` and `APP_NAME` with:
155158

156159
<div class="termy">
157160

@@ -174,7 +177,7 @@ And the `items_per_user` would keep its default value of `50`.
174177

175178
## Settings in another module
176179

177-
You could put those settings in another module file as you saw in [Bigger Applications - Multiple Files](bigger-applications.md){.internal-link target=_blank}.
180+
You could put those settings in another module file as you saw in [Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}.
178181

179182
For example, you could have a file `config.py` with:
180183

@@ -189,7 +192,7 @@ And then use it in a file `main.py`:
189192
```
190193

191194
!!! tip
192-
You would also need a file `__init__.py` as you saw on [Bigger Applications - Multiple Files](bigger-applications.md){.internal-link target=_blank}.
195+
You would also need a file `__init__.py` as you saw on [Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}.
193196

194197
## Settings in a dependency
195198

@@ -207,18 +210,19 @@ Coming from the previous example, your `config.py` file could look like:
207210

208211
Notice that now we don't create a default instance `settings = Settings()`.
209212

210-
Instead we declare its type as `Settings`, but the value as `None`.
211-
212213
### The main app file
213214

214-
Now we create a dependency that returns the `settings` object if we already created it.
215-
216-
Otherwise we create a new one, assign it to `config.settings` and then return it from the dependency.
215+
Now we create a dependency that returns a new `config.Settings()`.
217216

218-
```Python hl_lines="8 9 10 11 12"
217+
```Python hl_lines="5 11 12"
219218
{!../../../docs_src/settings/app02/main.py!}
220219
```
221220

221+
!!! tip
222+
We'll discuss the `@lru_cache()` in a bit.
223+
224+
For now you can assume `get_settings()` is a normal function.
225+
222226
And then we can require it from the *path operation function* as a dependency and use it anywhere we need it.
223227

224228
```Python hl_lines="16 18 19 20"
@@ -275,20 +279,102 @@ Here we create a class `Config` inside of your Pydantic `Settings` class, and se
275279
!!! tip
276280
The `Config` class is used just for Pydantic configuration. You can read more at <a href="https://pydantic-docs.helpmanual.io/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>
277281

278-
### Creating the settings object
282+
### Creating the `Settings` only once with `lru_cache`
283+
284+
Reading a file from disk is normally a costly (slow) operation, so you probably want to do it only once and then re-use the same settings object, instead of reading it for each request.
285+
286+
But every time we do:
287+
288+
```Python
289+
config.Settings()
290+
```
291+
292+
a new `Settings` object would be created, and at creation it would read the `.env` file again.
293+
294+
If the dependency function was just like:
295+
296+
```Python
297+
def get_settings():
298+
return config.Settings()
299+
```
279300

280-
Reading a file from disk is normally a costly (slow) operation, so you probably want to do it only once and then re-use the same settings, instead of reading it for each request.
301+
we would create that object for each request, and we would be reading the `.env` file for each request. ⚠️
281302

282-
Because of that, in the dependency function, we first check if we already have a `settings` object, and create a new one (that could read from disk) only if it's still `None`, so, it would happen only the first time:
303+
But as we are using the `@lru_cache()` decorator on top, the `Settings` object will be created only once, the first time it's called. ✔️
283304

284-
```Python hl_lines="9 10 11 12"
305+
```Python hl_lines="1 10"
285306
{!../../../docs_src/settings/app03/main.py!}
286307
```
287308

309+
Then for any subsequent calls of `get_settings()` in the dependencies for the next requests, instead of executing the internal code of `get_settings()` and creating a new `Settings` object, it will return the same object that was returned on the first call, again and again.
310+
311+
#### `lru_cache` Technical Details
312+
313+
`@lru_cache()` modifies the function it decorates to return the same value that was returned the first time, instead of computing it again, executing the code of the function every time.
314+
315+
So, the function below it will be executed once for each combination of arguments. And then the values returned by each of those combinations of arguments will be used again and again whenever the function is called with exactly the same combination of arguments.
316+
317+
For example, if you have a function:
318+
319+
```Python
320+
@lru_cache()
321+
def say_hi(name: str, salutation: str = "Ms."):
322+
return f"Hello {salutation} {name}"
323+
```
324+
325+
your program could execute like this:
326+
327+
```mermaid
328+
sequenceDiagram
329+
330+
participant code as Code
331+
participant function as say_hi()
332+
participant execute as Execute function
333+
334+
rect rgba(0, 255, 0, .1)
335+
code ->> function: say_hi(name="Camila")
336+
function ->> execute: execute function code
337+
execute ->> code: return the result
338+
end
339+
340+
rect rgba(0, 255, 255, .1)
341+
code ->> function: say_hi(name="Camila")
342+
function ->> code: return stored result
343+
end
344+
345+
rect rgba(0, 255, 0, .1)
346+
code ->> function: say_hi(name="Rick")
347+
function ->> execute: execute function code
348+
execute ->> code: return the result
349+
end
350+
351+
rect rgba(0, 255, 0, .1)
352+
code ->> function: say_hi(name="Rick", salutation="Mr.")
353+
function ->> execute: execute function code
354+
execute ->> code: return the result
355+
end
356+
357+
rect rgba(0, 255, 255, .1)
358+
code ->> function: say_hi(name="Rick")
359+
function ->> code: return stored result
360+
end
361+
362+
rect rgba(0, 255, 255, .1)
363+
code ->> function: say_hi(name="Camila")
364+
function ->> code: return stored result
365+
end
366+
```
367+
368+
In the case of our dependency `get_settings()`, the function doesn't even take any arguments, so it always returns the same value.
369+
370+
That way, it behaves almost as if it was just a global variable. But as it uses a dependency function, then we can override it easily for testing.
371+
372+
`@lru_cache()` is part of `functools` which is part of Python's standard library, you can read more about it in the <a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">Python docs for `@lru_cache()`</a>.
373+
288374
## Recap
289375

290376
You can use Pydantic Settings to handle the settings or configurations for your application, with all the power of Pydantic models.
291377

292378
* By using a dependency you can simplify testing.
293379
* You can use `.env` files with it.
294-
* Saving the settings in a variable lets you avoid reading the dotenv file again and again for each request.
380+
* Using `@lru_cache()` lets you avoid reading the dotenv file again and again for each request, while allowing you to override it during testing.

docs_src/settings/app02/config.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,3 @@ class Settings(BaseSettings):
55
app_name: str = "Awesome API"
66
admin_email: str
77
items_per_user: int = 50
8-
9-
10-
settings: Settings = None

docs_src/settings/app02/main.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1+
from functools import lru_cache
2+
13
from fastapi import Depends, FastAPI
24

35
from . import config
46

57
app = FastAPI()
68

79

10+
@lru_cache()
811
def get_settings():
9-
if config.settings:
10-
return config.settings
11-
config.settings = config.Settings()
12-
return config.settings
12+
return config.Settings()
1313

1414

1515
@app.get("/info")

docs_src/settings/app03/config.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,3 @@ class Settings(BaseSettings):
88

99
class Config:
1010
env_file = ".env"
11-
12-
13-
settings: Settings = None

docs_src/settings/app03/main.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1+
from functools import lru_cache
2+
13
from fastapi import Depends, FastAPI
24

35
from . import config
46

57
app = FastAPI()
68

79

10+
@lru_cache()
811
def get_settings():
9-
if config.settings:
10-
return config.settings
11-
config.settings = config.Settings()
12-
return config.settings
12+
return config.Settings()
1313

1414

1515
@app.get("/info")

0 commit comments

Comments
 (0)