Zmienne globalne w Pythonie wydają się proste, dopóki kod nie zaczyna rosnąć i nagle nie wiadomo, skąd bierze się dana wartość oraz kto ją zmienia. W tym tekście pokazuję, jak działa zasięg nazw, kiedy potrzebny jest global, dlaczego łatwo wpaść w pułapkę zasięgu lokalnego i jakie rozwiązania są zwykle lepsze w skryptach oraz aplikacjach webowych. Najważniejsza różnica jest praktyczna: odczyt globalnej wartości to nie to samo co jej modyfikacja.
Najważniejsze rzeczy, które trzeba wiedzieć od razu
- Globalna nazwa w Pythonie to taka, która jest zdefiniowana na poziomie modułu i może być odczytana z funkcji bez dodatkowych deklaracji.
- Jeśli chcesz zmienić taką nazwę wewnątrz funkcji, zwykle potrzebujesz
global, bo przypisanie domyślnie tworzy nazwę lokalną. - Największą pułapką nie jest sam odczyt, tylko sytuacja, w której funkcja przez przypadek zaczyna zasłaniać zmienną zewnętrzną.
-
globaldziała tylko dla bieżącego modułu, a nie jako uniwersalny skrót do dowolnego stanu w programie. - W kodzie produkcyjnym, zwłaszcza webowym, lepiej często przekazać dane jawnie albo zamknąć stan w obiekcie niż polegać na wspólnych zmiennych.

Jak działa zasięg i dlaczego lokalna nazwa może przysłonić globalną
Python nie szuka nazw przypadkowo. Najpierw sprawdza zakres lokalny funkcji, potem zakresy otaczające, następnie moduł i dopiero na końcu nazwy wbudowane. To oznacza, że globalna zmienna jest widoczna w funkcji, jeśli tylko ją odczytujesz, ale sytuacja zmienia się w momencie przypisania.
Jeżeli w treści funkcji pojawi się jakiekolwiek przypisanie do danej nazwy, interpreter traktuje ją jako lokalną dla całej funkcji. W praktyce właśnie z tego bierze się większość nieporozumień związanych z globalami.
x = 10
def foo():
print(x)
x += 1Na pierwszy rzut oka wygląda to niewinnie, ale Python uzna x za lokalne, bo w funkcji występuje przypisanie. Gdy dochodzi do print(x), lokalna zmienna jeszcze nie istnieje, więc pojawia się błąd. To nie jest kaprys interpretera, tylko konsekwencja reguły, która ma chronić przed przypadkowym nadpisaniem stanu z zewnątrz.
Warto pamiętać też o jednej ważnej rzeczy: na poziomie modułu wszystkie nazwy są już globalne, więc global nie ma tam sensu. Taka deklaracja ma znaczenie dopiero wewnątrz funkcji, gdy chcesz odwołać się do wiązania z poziomu modułu. To prowadzi nas prosto do pytania, kiedy deklaracja naprawdę jest potrzebna, a kiedy tylko kusi, żeby użyć jej na skróty.
Kiedy potrzebujesz global, a kiedy lepiej go nie używać
Jeżeli funkcja ma tylko odczytać wartość z modułu, niczego nie deklarujesz. Jeżeli ma ją zmienić, wtedy global staje się potrzebne. To prosta zasada, ale właśnie na tym etapie najłatwiej zbudować kod, który działa dziś, a za tydzień jest już trudny do utrzymania.
counter = 0
def read_counter():
return counter
def increment_counter():
global counter
counter += 1Ten przykład pokazuje podstawowy mechanizm: odczyt nie wymaga dodatkowych instrukcji, ale modyfikacja tak. Sam fakt, że coś można zrobić, nie oznacza jednak, że warto. Ja zwykle traktuję global jako znak, że funkcja zaczyna zarządzać stanem współdzielonym przez cały moduł, a to już wymaga ostrożności.
Gdy problem dotyczy zagnieżdżonych funkcji
Jeśli chcesz zmieniać zmienną z funkcji otaczającej, używasz nonlocal, a nie global. To ważne rozróżnienie, bo oba słowa kluczowe rozwiązują podobny problem, ale w różnych miejscach łańcucha zakresów.
def outer():
total = 0
def inner():
nonlocal total
total += 1
inner()
return totalnonlocal sięga do najbliższego otaczającego zakresu funkcji, a global do modułu. Jeśli pomylisz te dwa mechanizmy, kod może jeszcze wyglądać poprawnie, ale zacznie zachowywać się inaczej niż planowałeś. Gdy już to rozróżnisz, łatwiej przejść do konkretnych przykładów, które zwykle wyjaśniają temat szybciej niż sama definicja.
Przykłady, które najczęściej wyjaśniają temat do końca
Najlepiej rozumie się ten temat przez porównanie trzech sytuacji: tylko odczyt, przypisanie i mutowanie obiektu. Na papierze wyglądają podobnie, ale dla interpretera to zupełnie inne operacje.
| Operacja | Czy potrzebujesz global
|
Co się dzieje |
|---|---|---|
| Odczyt nazwy | Nie | Python szuka wartości w zewnętrznych zakresach. |
x = ... |
Tak, jeśli chodzi o zmienną z modułu | Tworzysz lokalne wiązanie, chyba że użyjesz global. |
lista.append(...) |
Zwykle nie | Zmienia się obiekt, ale nie sama nazwa. |
lista = lista + [...] |
Tak | Powstaje nowe przypisanie do nazwy. |
Odczyt bez przypisania
To najprostszy przypadek. Jeśli w funkcji tylko korzystasz z wartości, globalna nazwa jest widoczna i nie musisz niczego dopisywać. Dzięki temu konfigurację, stałe albo proste liczniki da się odczytać bez dodatkowego szumu w kodzie.
tax_rate = 0.23
def price_with_tax(price):
return price * (1 + tax_rate)Ten kod działa, bo tax_rate nie jest w funkcji nadpisywane. W praktyce to jedyny scenariusz, w którym globalne dane bywają naprawdę wygodne. Zaraz obok niego stoi jednak przypadek odwrotny, czyli przypisanie wewnątrz funkcji, które zmienia wszystko.
Przypisanie tworzy lokalną nazwę
Jeśli funkcja ma zmienić wartość z modułu, bez global nie obejdzie się. To właśnie dlatego poniższy kod jest błędny, mimo że wygląda logicznie:
value = 5
def bad():
print(value)
value = 6Python uzna value za lokalne, a wcześniejszy odczyt zakończy się błędem. To jeden z tych momentów, w których początkujący myślą, że interpreter „nie widzi” zmiennej globalnej, a w rzeczywistości interpreter widzi za dużo i stosuje regułę zakresu w sposób konsekwentny. Ostatni przypadek jest jeszcze bardziej mylący, bo dotyczy obiektów mutowalnych.
Mutowanie obiektu nie zawsze wymaga global
Jeżeli zmieniasz zawartość listy albo słownika, ale nie przypisujesz nowej nazwy, global nie jest potrzebny. To dlatego append, extend czy aktualizacja klucza w słowniku działają bez deklaracji:
items = []
def add_item(item):
items.append(item)Tu nazwa items nie jest nadpisywana, więc Python nie traktuje jej jako lokalnej. Problem zaczyna się dopiero wtedy, gdy tworzysz nowy obiekt i przypisujesz go do tej samej nazwy. To rozróżnienie między mutowaniem a ponownym wiązaniem nazwy jest jednym z najważniejszych w całym temacie. Gdy je rozumiesz, łatwiej zauważyć typowe błędy zanim staną się usterką w większym projekcie.
Najczęstsze błędy przy pracy ze stanem modułu
W code review najczęściej widzę cztery schematy, które powtarzają się bez względu na poziom doświadczenia. Każdy z nich wygląda na drobny detal, ale później potrafi kosztować godzinę debugowania.
- UnboundLocalError - funkcja czyta nazwę, a potem przypisuje do niej wartość, więc Python uznaje ją za lokalną od samego początku.
-
Spóźniony
global- deklaracja pojawia się po pierwszym użyciu nazwy w tym samym zakresie, co kończy się błędem składniowym. - Ukryte współdzielenie stanu - kilka funkcji modyfikuje ten sam obiekt, ale nikt nie ma jasnego właściciela odpowiedzialności.
- Globalny stan w aplikacji webowej - przy wielu procesach lub wątkach wartość może nie być wspólna tam, gdzie się tego spodziewasz, a dane requestowe nie powinny i tak żyć w module.
name = "Ala"
def broken():
print(name)
global name
name = "Ola"Ten przykład pokazuje jeszcze jedną rzecz: global musi pojawić się zanim cokolwiek zrobisz z nazwą w danym zakresie. Jeśli użyjesz jej wcześniej, interpreter zatrzyma program. Dodatkowy problem pojawia się wtedy, gdy zmienna globalna ma nazwę podobną do wbudowanej funkcji, na przykład list albo id. Taki wybór nie psuje programu od razu, ale skutecznie utrudnia czytanie i debugowanie kodu.
Jeśli pracujesz nad aplikacją webową, dochodzi jeszcze kwestia współbieżności. Globalny licznik albo koszyk w pamięci procesu nie jest dobrym magazynem danych między żądaniami, bo każdy worker może mieć własną kopię, a równoległe zapisy wymagają synchronizacji. To dobry moment, żeby porównać globalny stan z rozwiązaniami, które zwykle składają się lepiej w większych projektach.
Lepsze alternatywy dla globali w realnym kodzie
W praktyce najczęściej wybieram jedną z kilku dróg: przekazuję dane jawnie, zwracam wynik albo zamykam stan w obiekcie. Każda z tych opcji ma swój koszt, ale też daje znacznie lepszą kontrolę nad przepływem danych niż rozproszony stan modułu.
| Rozwiązanie | Kiedy działa najlepiej | Co zyskujesz | Ograniczenie |
|---|---|---|---|
| Argumenty funkcji | Gdy dane są wejściem do obliczenia | Jawność i prostsze testy | Więcej parametrów w podpisie funkcji |
| Zwracanie wyniku | Gdy funkcja ma coś policzyć lub przetworzyć | Mniej ukrytych efektów ubocznych | Trzeba czasem przebudować wywołania |
Obiekt lub dataclass
|
Gdy stan należy do jednego procesu biznesowego | Porządek i jedna odpowiedzialność | Wymaga trochę więcej struktury |
| Moduł konfiguracji | Gdy wartości są stałe lub zmieniają się rzadko | Centralne miejsce na ustawienia | Nie nadaje się do dynamicznego stanu użytkownika |
| Baza danych, cache, sesja | Gdy stan musi przetrwać dłużej niż proces | Trwałość i współdzielenie między instancjami | Większa złożoność i koszt infrastruktury |
Jeśli chcesz zachować prostotę, ale uniknąć globali, często wystarcza mały obiekt z jawnie przekazywanym stanem:
from dataclasses import dataclass
@dataclass
class AppState:
counter: int = 0
def increment(state: AppState):
state.counter += 1Taki układ jest czytelniejszy, bo od razu widać, kto odpowiada za dane. W małym skrypcie może to wyglądać na nadmiarowy formalizm, ale w projekcie, który będzie rósł, zwykle zwraca się bardzo szybko. Z tego miejsca już łatwo przejść do pytania, kiedy global w ogóle da się obronić.
Kiedy global ma sens i jak trzymać go w ryzach
Nie demonizuję globali absolutnie. W małym skrypcie, jednorazowym narzędziu albo prostym module pomocniczym mogą być rozsądnym skrótem, jeśli spełniają kilka warunków.
- Stan ma jednego właściciela i jest modyfikowany w jednym miejscu.
- Wartość nie zależy od pojedynczego żądania użytkownika ani od równoległych zadań.
- Zmiana stanu jest mała, przewidywalna i łatwa do przetestowania.
- Kod nadal pozostaje czytelny bez zgadywania, skąd bierze się dana wartość.
Gdy któryś z tych punktów przestaje być prawdziwy, global przestaje być wygodnym skrótem, a zaczyna być technicznym długiem. Wtedy lepiej przenieść stan do obiektu, przekazać go jako argument albo wynieść poza proces, jeśli ma być współdzielony szerzej.
Moja praktyczna zasada jest prosta: jeśli po miesiącu nie potrafię spojrzeć na funkcję i od razu powiedzieć, kto może zmienić jej dane wejściowe, to prawdopodobnie mam za dużo ukrytego stanu. Właśnie dlatego w Pythonie globalne zmienne traktuję jako narzędzie pomocnicze, a nie domyślny sposób projektowania kodu.