Skip to content
CodeKruPro

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.

  1. Local: names assigned in the current function.
  2. Enclosing: names in outer functions, for nested functions.
  3. Global: names at the top level of the module.
  4. Built-in: len, print, range and 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

  • name is public.
  • _name is internal, roughly "protected". Nothing stops access, but readers and linters (programs that check code style) respect it.
  • __name triggers name mangling: inside class Account it 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 += 1 on a global inside a function and getting UnboundLocalError.
  • Updating a class attribute through self.
  • Calling _name or __name "private" as if Python enforced it.
  • Naming a variable list, id or type.
  • 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

StatusQuestionDifficultyAsked in
Not startedWhat do leading underscores mean?EasyAsked in: Accenture, Capgemini, Infosys +1
Not startedPython has no access modifiers: what does it use?Easy-MediumAsked in: Accenture, Capgemini, Infosys +1
Pro+Variable scope: local, enclosing, global and built-inMediumAsked in: TCS
Pro+Changing a global counter inside a functionMediumAsked in: TCS
Pro+@staticmethod, @classmethod and class attributesMediumAsked in: Accenture, Infosys, TCS +1
Pro+A classmethod called on a subclassMedium
Pro+final, finally and finalize: the Python equivalentsEasy-MediumAsked in: Accenture, Capgemini, Infosys +1
Pro+Keywords, soft keywords and built-insEasy-Medium
Not startedNot startedIn progressIn progressSolvedSolvedPro+Pro+

The first 2 questions of each topic are free. Pro+ opens every question.

Back to the plan