Your language · Python
Keywords, access and scope
static, final and access modifiers, or scope rules and naming conventions.
Learn: Scope, naming and access in Python8 minute read
This topic covers where a name is visible (its scope), how Python marks names as internal, and Python's versions of Java's static, final and access modifiers. Python has none of those Java keywords, so interviewers ask how Python does the same jobs. The scope rules also explain a common error, UnboundLocalError, which turns up when a helper tries to update a counter. (Java's static means "belongs to the class, not to one object"; final means "cannot change"; access modifiers such as private control who can use a member.)
Step 1: scope and the LEGB rule
When Python reads a name, it searches four scopes in this order:
A scope is the area of code where a name can be used. A module is a Python file.
- Local: names assigned in the current function.
- Enclosing: names in outer functions, for nested functions.
- Global: names at the top level of the module.
- Built-in:
len,print,rangeand the rest.
Assignment follows a different rule. If a function assigns to a name anywhere in its body, that name is local for the whole function. Reading it before the assignment raises UnboundLocalError, even when a global with the same name exists. global name makes the assignment change the module-level name, and nonlocal name does the same for an enclosing function. Only modules, functions, classes and comprehensions (one-line loops that build a list, such as [x for x in items]) create scopes; loops and if blocks do not.
retries = 0
def broken():
retries += 1 # assignment makes retries local
def fixed():
global retries
retries += 1
try:
broken()
except UnboundLocalError as e:
print("UnboundLocalError:", e)
fixed()
print("retries:", retries)
def outer():
level = "enclosing"
def inner():
return level # read from the enclosing scope
return inner()
print(outer())
for i in range(3):
pass
print("loop variable after loop:", i)
UnboundLocalError: cannot access local variable 'retries' where it is not associated with a value retries: 1 enclosing loop variable after loop: 2
Avoid global in test code: module-level state shared between tests makes them depend on each other. Pass values in and return results, or use fixtures (pytest functions that prepare things for a test).
Step 2: access by naming convention
nameis public._nameis internal, roughly "protected". Nothing stops access, but readers and linters (programs that check code style) respect it.__nametriggers name mangling: inside classAccountit becomes_Account__name, so a subclass cannot clash with it by accident. It is not security.__name__with underscores on both sides is a special ("dunder", short for double underscore) name that Python itself uses. It is never mangled.
To control access for real, use @property (a decorator, a label above a method, that lets the method be read like an attribute): reads go through a method, a setter can check values, and leaving the setter out makes the attribute read-only.
Step 3: Python's version of static
Class attributes, written in the class body (the indented block under class), are shared by all objects, like Java's static fields. Update them through the class (Account.created += 1); writing through self creates an attribute on the object instead. A @staticmethod receives neither the object nor the class; it is a plain function kept in the class. A @classmethod receives the class as cls and is used for alternative constructors (another way to build an object) such as Account.from_dict(data).
class Account:
created = 0
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance
self.__pin = "1234"
Account.created += 1
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("balance cannot be negative")
self._balance = value
@staticmethod
def parse_amount(text):
return float(text.replace(",", ""))
@classmethod
def from_dict(cls, data):
return cls(data["owner"], cls.parse_amount(data["balance"]))
acc = Account.from_dict({"owner": "Asha", "balance": "1,200.50"})
print(acc.owner, acc.balance, Account.created)
try:
acc.balance = -5
except ValueError as e:
print("ValueError:", e)
try:
print(acc.__pin)
except AttributeError:
print("no attribute __pin outside the class")
print(acc._Account__pin)
Asha 1200.5 1 ValueError: balance cannot be negative no attribute __pin outside the class 1234
Module-level functions often replace a class full of static methods; a waits.py module is more natural in Python.
Step 4: final, finally and keywords
Python has no final keyword. Constants are written in capitals, and typing.Final lets a type checker (a tool that checks types before the code runs) report a reassignment, though Python itself allows it. finally works as in Java. The nearest thing to finalize (Java's old clean-up method) is __del__, whose timing is not guaranteed, so clean up with with, try/finally or a fixture instead (see Exceptions).
Python 3.12 has 35 keywords, such as def, lambda, nonlocal and None. match, case, _ and type are soft keywords, special only in certain places. print and list are built-in names, not keywords, so list = [1, 2] works but hides the built-in.
How to answer in an interview
When asked "what is the Python version of X?", map it directly: "Access modifiers become underscore conventions plus properties. Static fields become class attributes, and static methods become @staticmethod, @classmethod or module functions. final becomes an upper-case name and typing.Final." For a scope question, name the LEGB order and explain why assigning a name makes it local.
Common mistakes
- Writing
total += 1on a global inside a function and gettingUnboundLocalError. - Updating a class attribute through
self. - Calling
_nameor__name"private" as if Python enforced it. - Naming a variable
list,idortype. - Relying on
__del__to quit a browser.
Recap
Names are found in LEGB order, and any assignment makes a name local unless you declare it global or nonlocal. Underscores show intent; __name is mangled. Class attributes are shared, @staticmethod has no self, and @classmethod gets the class. Constants are a naming convention.
8 questions0/8 solved
| Status | Question | Difficulty | Asked in |
|---|---|---|---|
| Not started | What do leading underscores mean? | Easy | Asked in: Accenture, Capgemini, Infosys +1 |
| Not started | Python has no access modifiers: what does it use? | Easy-Medium | Asked in: Accenture, Capgemini, Infosys +1 |
| Pro+ | Variable scope: local, enclosing, global and built-in | Medium | Asked in: TCS |
| Pro+ | Changing a global counter inside a function | Medium | Asked in: TCS |
| Pro+ | @staticmethod, @classmethod and class attributes | Medium | Asked in: Accenture, Infosys, TCS +1 |
| Pro+ | A classmethod called on a subclass | Medium | |
| Pro+ | final, finally and finalize: the Python equivalents | Easy-Medium | Asked in: Accenture, Capgemini, Infosys +1 |
| Pro+ | Keywords, soft keywords and built-ins | Easy-Medium |
The first 2 questions of each topic are free. Pro+ opens every question.