2

I would like to have a global variable limited to a FastAPI request execution but common to multiple modules. Below is a small example to explain the problem:

I built a very simple app with a main file app.py and a module mymodule. For each test i launch the app in uvicorn with 1 worker and then open 2 python consoles to call for long and short call requests.get("http://localhost:8000/fakelongcall", {"configtest": "long"}), requests.get("http://localhost:8000/fakeshortcall", {"configtest": "short"}).

First situation: it's working but the global variable GLOBAL_VAR is not in a module thus not accessible by an other module (I may be wrong on that).

app.py

import time

from fastapi import FastAPI
GLOBAL_VAR = "default"

app = FastAPI()


@app.get("/fakelongcall")
def fakelongcall(configtest: str):
    cpt = 0
    while cpt < 10:
        print(GLOBAL_VAR)
        time.sleep(1)
        cpt = cpt + 1


@app.get("/fakeshortcall")
def fakeshortcall(configtest: str):
    GLOBAL_VAR = configtest
    print("Change done !")

output

INFO:     Started server process [34182]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
default
default
Change done !
INFO:     127.0.0.1:52250 - "GET /fakeshortcall?configtest=short HTTP/1.1" 200 OK
default
default
default
default
default
default
default
default

Second situation: both calls are changing the same variable which is not expected but the variable is accessible in a module which is nice.

app.py

import time

from fastapi import FastAPI
import mymodule

app = FastAPI()


@app.get("/fakelongcall")
def fakelongcall(configtest: str):
    cpt = 0
    while cpt < 10:
        print(mymodule.GLOBAL_VAR)
        time.sleep(1)
        cpt = cpt + 1


@app.get("/fakeshortcall")
def fakeshortcall(configtest: str):
    mymodule.GLOBAL_VAR = configtest
    print("Change done !")

mymodule.py

GLOBAL_VAR = "default"

output

INFO:     Started server process [33994]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
default
default
default
Change done !
INFO:     127.0.0.1:52240 - "GET /fakeshortcall?configtest=short HTTP/1.1" 200 OK
short
short
short
short
short
short
short

Why is this happening ? How can one worker execute 2 calls simultaneously ? What can I do to obtain a module variable that is not shared among different API requests ? Why embedding the same code in a module changes the behavior ?

Thanks in advance for your help.

Guillaume B
  • 23
  • 1
  • 4

2 Answers2

1

This is a classic pitfall when dealing with global variables. From FAQ:

In Python, variables that are only referenced inside a function are implicitly global. If a variable is assigned a value anywhere within the function’s body, it’s assumed to be local unless explicitly declared as global.

In the second case, you are accessing a member of the imported module, which does not create a local variable.

To fix it use the global keyword:

def foo():
    global GLOBAL_VAR
    GLOBAL_VAR = configtest
alex_noname
  • 26,459
  • 5
  • 69
  • 86
  • I got the same results as in second case with this solution. To be more general my question is: "What is the best way to define cross-module but execution specific variables in python with fastapi ?" – Guillaume B May 25 '21 at 14:28
  • Perhaps I did not quite understand your question. Look at the [context variables](https://docs.python.org/3/library/contextvars.html). Perhaps this is what you need. – alex_noname May 25 '21 at 14:35
  • Thanks alex ! Contextvars was the solution to obtain thread specific "global" variables. – Guillaume B May 25 '21 at 15:55
  • may be helpful https://stackoverflow.com/questions/63105799/understanding-python-contextvars/63131230 – alex_noname May 25 '21 at 16:17
0

So thanks to alex_noname. It works with contextvars.

app.py

import time

from fastapi import FastAPI
import mymodule

app = FastAPI()


@app.get("/fakelongcall")
def fakelongcall(configtest: str):
    cpt = 0
    mymodule.var.set(configtest)
    while cpt < 10:
        print(mymodule.var.get())
        time.sleep(1)
        cpt = cpt + 1


@app.get("/fakeshortcall")
def fakeshortcall(configtest: str):
    mymodule.var.set(configtest)
    print("Change done !")

mymodule.py

from contextvars import ContextVar
var = ContextVar("var")

output

INFO:     Started server process [37237]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
long
long
long
Change done !
INFO:     127.0.0.1:38680 - "GET /fakeshortcall?configtest=short HTTP/1.1" 200 OK
long
long
long
long
long
long
long
long
INFO:     127.0.0.1:38678 - "GET /fakelongcall?configtest=long HTTP/1.1" 200 OK
Guillaume B
  • 23
  • 1
  • 4