What a Variable Actually Is
In Python, a variable is a name bound to an object. When you write age = 25, Python creates an integer object with the value 25 somewhere in memory, then binds the name age to it. The variable is the label, not the box. The single = here is the assignment operator — it binds a name to a value. It is not the same as ==, which is the equality operator used to compare two values. Mixing them up is one of the most common early mistakes: age = 25 stores the value, while age == 25 asks whether age is currently equal to 25 and produces True or False.
Python figures out the type automatically based on the value you assign. This is called dynamic typing — you do not declare a type upfront. The consequence is that any variable can be rebound to a completely different type at any time, which matters for security: a variable that starts holding a string username could later be rebound to hold something else entirely if you are not careful with scope and reassignment.
In statically typed languages like Java or C, a variable declared as an integer can only ever hold an integer. Python has no such restriction. The name is just a label, and you can point that label at any object at any time — regardless of what it was pointing to before.
For example:
username = "alice" — username points to a stringusername = 42 — username now points to an integerusername = ["a", "b"] — username now points to a listPython does not warn you. Each assignment silently discards the previous binding. The security implication is that if a variable name like
user_token gets reassigned somewhere later in the code — intentionally or by accident — any code that reads it afterwards gets the new value, not the original one. Descriptive, consistently used names and keeping variables scoped as tightly as possible both help prevent this kind of silent rebinding.When you write b = a, both names point to the same object. No copy is made. If that object is mutable (like a list or dictionary), a change through one name is visible through the other. This is how sensitive data can leak between parts of a program that share a reference without realising it.
Naming Rules, Conventions, and Why They Matter for Security
Python enforces a short set of hard rules for variable names, and the Python community has a style standard — PEP 8 — that goes further. Both matter for writing readable, auditable code. But beyond syntax and style, the question of how descriptive your names should be has a direct security answer worth understanding in depth.
Variable names must start with a letter or an underscore — never a digit.
After the first character, they can contain letters, digits, and underscores only.
Names are case-sensitive: token, Token, and TOKEN are three distinct names.
Reserved keywords cannot be used: if, for, class, return, True, False, None, and others.
Avoid shadowing Python built-ins: names like list, input, id, and type are legal but overwrite tools you will want later.
Variables and functions: snake_case — lowercase words separated by underscores. Example: user_token, max_retries.
Constants: ALL_CAPS_WITH_UNDERSCORES. Example: MAX_LOGIN_ATTEMPTS, API_BASE_URL.
Classes: PascalCase — each word capitalised, no underscores. Example: UserSession.
Private / internal use: single leading underscore: _internal_token. This is a convention, not enforced by Python.
Never use the single characters l (lowercase L), O (uppercase o), or I (uppercase i) as variable names — they are visually indistinguishable from the digits 1 and 0.
Should You Obscure Your Variable Names to Confuse Attackers?
This question comes up more often than you might expect, and it deserves a direct answer: no. Deliberately obscuring variable names in your own codebase does not protect you from attackers, and it actively harms your ability to defend your own code. This principle has a formal name in security — it is called security through obscurity — and the security community has rejected it as a primary defense for decades.
Here is why obscuring names does not work as a defense. An attacker who has reached your Python source code has already bypassed your real defenses. At that point, renaming user_token to x4b delays them by minutes at most — and makes your code impossible for your own team to audit, maintain, or review for vulnerabilities. You are trading long-term defensibility for negligible friction against an attacker.
More importantly, obscure naming is a technique attackers themselves use — not defenders. When malware authors write Python-based infostealers or backdoors, they rename variables and functions to meaningless strings precisely to slow down security researchers and evade static analysis tools. If your own codebase uses the same technique, you are making it harder for your security team, your auditors, and automated scanners to find real vulnerabilities in it.
Kerckhoffs' Principle — one of the foundational ideas in modern cryptography — states that a system should be secure even if everything about it except the key is public knowledge. The security of your application should rest on proper access controls, encrypted secrets, validated inputs, and correct logic — not on someone failing to understand your variable names. If your code is only safe because an attacker cannot read it, it is not actually safe.
Descriptive Names Are a Security Practice
The correct direction is the opposite of obscurity: names should be as descriptive as possible, and this is a security requirement, not just a style preference. Here is why.
Security audits depend on readable code. When a security engineer reviews your code for vulnerabilities — or when you review it yourself six months later — the names of variables are the primary signal for what the code is doing. A variable named x tells a reviewer nothing. A variable named raw_user_input immediately signals that the value came directly from a user and has not been validated or sanitized. That difference can be the difference between catching an injection vulnerability and missing it.
Misleading names introduce bugs that become vulnerabilities. If a variable is named validated_input but actually holds raw unvalidated data, any developer who reads that name will assume it is safe to use without further checking. The name creates a false sense of security. This is not hypothetical — it is one of the most common ways that security bugs get introduced and then overlooked in code review.
Automated tools rely on names. Static analysis tools, linters, and secret scanning tools use variable names as one of their primary signals. A variable named password or api_key will trigger scrutiny from tools like Bandit and GitHub's secret scanning. That scrutiny is useful — it will catch if you accidentally hardcode a value. Obscure names bypass those tools entirely, leaving real vulnerabilities undetected.
Each pair of names above tells a story about what has happened to the data. raw_user_input is dangerous — treat it as untrusted. sanitized_username has been processed. plaintext_password must never be logged or stored. hashed_password is safe to persist. unverified_token must be verified before trusting its claims. These names make the security state of the data visible in the code itself, without any comments required.
The most security-useful naming pattern is to encode the state of your data in the variable name — especially for data that flows through validation, sanitization, or transformation steps. raw_, sanitized_, validated_, encoded_, hashed_, encrypted_, decrypted_, unverified_, verified_ — prefixes like these turn your variable names into a record of what has been done to the data and what still needs to happen. Any step that skips a transformation becomes immediately visible to a reviewer.
Names That Are Too Long Are Also a Problem
Descriptive does not mean verbose to the point of obscuring logic. A name like the_variable_that_holds_the_raw_unvalidated_string_input_from_the_user_login_form is not more secure — it is unreadable, which produces the same problem as a name that is too short. The goal is precision, not length. Use the minimum number of words needed to make the variable's purpose and state unambiguous.
A useful test: if a colleague who knows the codebase but did not write this function can read the variable name and immediately know what it holds, what state it is in, and whether it is safe to pass to another function — the name is right. If they have to trace back through the code to answer those questions, the name needs work.
The last three lines show what not to do. Single-letter names and meaningless abbreviations tell the reader nothing about what the variable holds. In a code review or security audit, a reviewer looking at x cannot tell whether it holds a username, a file path, or a secret key. Ambiguous names do not protect you from an attacker — they protect an attacker from being caught by your reviewer.
The Boolean Naming Convention That Prevents Logic Errors
Boolean variables deserve special attention because naming them badly is one of the most common sources of logic errors that become security vulnerabilities. The convention is to prefix boolean names with a word that makes the true/false meaning immediately obvious: is_, has_, can_, was_, should_.
The second group looks shorter and neater. The problem is that if admin: reads ambiguously — is admin a boolean, a username string, an object, or a role code? if has_admin_role: is unambiguous: it is a boolean and it is True when the user has admin privileges. When access control logic depends on a boolean check, ambiguous naming creates conditions where logic errors go unnoticed. A reviewer scanning for authorisation bypass vulnerabilities looks for boolean guards — unclear names make those guards harder to verify at a glance.
Names like is_not_banned or not_verified create double-negation logic that is easy to get wrong: if not is_not_banned: is harder to reason about than if is_banned:. Negated booleans in access control checks — particularly checks that determine whether to allow an action — have caused real authorisation vulnerabilities. Name the positive condition clearly and negate it in code when needed rather than encoding the negation in the variable name.
Which of these is a valid Python variable name that also follows PEP 8 conventions?
auth_token follows snake_case, the PEP 8 convention for variable names. 2fa_token starts with a digit and would raise a SyntaxError. authToken is camelCase, used in other languages but not Python convention. AuthToken is PascalCase, reserved for class names.Constants — Values That Should Not Change
Python has no true constant — nothing in the language prevents you from reassigning a name written in ALL_CAPS. The capitalisation is a convention and a signal: it tells every reader that this value should be treated as fixed and that reassigning it would be a mistake.
Constants belong at the top of a module, outside any function or class. This makes them easy to audit in one place. A security reviewer looking at your code can immediately see what your application's fixed limits and endpoints are without searching through the entire file.
The capitalisation convention can tempt you to write API_KEY = "sk-abc123..." at the top of your file. This looks tidy and intentional — it is also a critical vulnerability. A constant is for values that are safe to appear in source code. API keys, database passwords, and tokens are not. Those belong in environment variables or a secrets manager. Lesson 01 covered .env files and why they must never be committed — the same principle applies here.
The Four Variable Scopes — Local, Enclosing, Global, Built-in
A variable's scope is the region of code where that name is valid and accessible. Python uses a four-level lookup order called LEGB: Local, Enclosing, Global, Built-in. When Python encounters a name, it searches these scopes in order and stops at the first match it finds.
Local Variables
A local variable is any name assigned inside a function. It only exists for the duration of that function call. Once the function returns, the local scope is discarded entirely — the name and the object it pointed to are both gone (unless the object is referenced elsewhere).
The last line raises a NameError. is_valid only exists inside validate_token. This is scope working correctly — it is a boundary that limits what other code can see. From a security standpoint, local variables are your friend: sensitive data held in a local variable cannot be read by any code outside that function.
Global Variables
A global variable is defined at the module level — outside any function or class. It can be read by any function in that module without any special keyword. However, assigning to a global variable from inside a function requires the global keyword, or Python creates a new local variable instead.
Any function in a module can read a global variable. If a global holds sensitive data — an authentication token, a database connection string, a decrypted secret — every function in that module can access it, regardless of whether it needs to. This expands the attack surface: a bug or injection in any function becomes a potential path to that data. Keep sensitive values as local as possible. Pass them as function arguments rather than relying on globals, and discard them as soon as they are no longer needed.
Reaching into global state from a function creates hidden dependencies that make code hard to test, audit, and reason about. If a function needs a value, pass it as a parameter. If a function needs to return updated state, return it. Functions that read and write globals are harder to audit in a security review because their behaviour depends on state that is not visible in the function itself.
Enclosing Scope and the nonlocal Keyword
The enclosing scope only applies when you have a function defined inside another function. The inner function can read variables from the outer function without any keyword. But if it wants to assign to one of those outer variables, it needs the nonlocal keyword, otherwise Python creates a new local variable in the inner scope and the outer variable is unchanged.
Each call to counter() increments the same count variable from the outer function's scope. The outer function has already returned, but count is kept alive because increment holds a reference to it. This pattern is called a closure — the inner function closes over the outer variable. Without nonlocal, count += 1 would raise an UnboundLocalError because Python would treat count as a new local variable that has not yet been assigned.
nonlocal reaches into the nearest enclosing function scope — not global. global reaches all the way to module level. You cannot use nonlocal to access a global variable, and you cannot use global to access an enclosing function's variable. If the variable you need is in an outer function, use nonlocal. If it is at module level, use global.
The UnboundLocalError — Python's Most Confusing Scope Behaviour
This error trips up even experienced developers. It happens because Python decides whether a variable is local or global at compile time, not at runtime. The rule is simple but surprising: if a name is assigned anywhere in a function, Python treats it as local throughout the entire function — even before the assignment line.
This raises UnboundLocalError: local variable 'attempts' referenced before assignment. Even though attempts exists as a global, the assignment on the second line of login causes Python to classify attempts as a local variable for the entire function. When print runs first and tries to read the local attempts, it has not been assigned yet. The fix is either to declare global attempts inside the function, or — the better design — to pass attempts as a parameter and return the new value.
The particularly disorienting version of this error is when code that was working breaks after you add a new line. You add attempts = attempts + 1 at the bottom of a function that previously read the global attempts without any problem. Suddenly the read that was working fine now raises an error — because the new assignment line changed how Python classified the variable for the whole function. Understanding that Python makes this decision at compile time explains why adding one line can break earlier lines.
Assignment Forms Most Tutorials Skip
The basic name = value assignment is just one of several ways Python lets you bind names. These others appear constantly in real code and have specific implications for how variables are created and updated.
Augmented Assignment
Augmented assignment operators combine an operation with an assignment in a single step. count += 1 means count = count + 1. All of the arithmetic operators have augmented forms: +=, -=, *=, /=, //=, %=, **=. There are also bitwise forms: &=, |=, ^=, >>=, <<=.
One important nuance: augmented assignment on a mutable object (like a list) modifies the object in place — it does not create a new object. On an immutable object (like a string or integer), it creates a new object and rebinds the name. This means list_a += [4] modifies list_a and every other name pointing to the same list, while count += 1 only affects count. This distinction matters when the same list is referenced from multiple variables.
Multiple Assignment and Tuple Unpacking
Python lets you assign multiple variables in a single line. This is called tuple unpacking — the right-hand side is unpacked and each value is bound to the corresponding name on the left.
The *rest syntax on the last line is called a starred assignment — it collects all remaining items into a list. You can also use it in the middle: first, *middle, last = some_list. This comes up frequently when parsing structured data and you only want certain fields.
Chained Assignment
Python allows chained assignment — a = b = 0 binds both names to the same object. This is convenient for initialising several counters at once, but carries a subtle risk with mutable objects: a = b = [] does not create two empty lists — it creates one list and binds two names to it. Mutating through a changes what b sees.
The output is ['secret'] — b contains the value even though only a was modified. With immutable types like integers and strings this is never a problem, but with lists and dictionaries it can cause unintended data sharing.
The Walrus Operator — Assignment Inside an Expression
Python 3.8 introduced the walrus operator (:=), formally called the assignment expression operator. It lets you assign a value to a name and use that value in the same expression. Regular assignment (=) is a statement — it cannot appear inside a condition or list comprehension. The walrus operator is an expression — it can.
Without the walrus operator you would call re.search() once in the if condition and then call it again inside the block to use the result, or assign it to a variable on a separate line first. The walrus operator assigns match and evaluates the truthiness of the result in a single step. The variable match is then available inside both the if and else branches.
The walrus operator is most useful when you would otherwise call the same function twice: once to check a condition and once to use the result. It is not a replacement for regular assignment. Overusing it makes code harder to read. The Python community generally recommends using it sparingly and only where it genuinely removes a redundant call.
The Hardcoded Secret Problem
The single most common variable-related security mistake in Python codebases is assigning a secret directly to a variable in source code. It looks harmless. It is not.
Once a secret is in source code, it travels everywhere that code travels: into version control, into backups, into build artifacts, into any clone made by any collaborator. Even if you delete the line later, the secret remains in git history. Automated scanners — run continuously by attackers and also by tools like GitHub's secret scanning — will find it. The variable name makes it worse: naming something API_KEY in all-caps signals to any reader that this is important and should not change, but Python does not enforce that, so any part of the code could reassign it accidentally.
os.getenv() reads from the process environment at runtime — the value never appears in your source code. The explicit check and raise on the last two lines ensures the application fails loudly at startup rather than running silently with a missing key, which would likely cause a confusing error deep in your logic later.
Use lowercase snake_case for variables that hold runtime secrets loaded from the environment: api_key, db_password. Reserve ALL_CAPS for values that are genuinely fixed and safe to appear in source code: MAX_RETRIES, ALLOWED_HOSTS. The visual distinction helps reviewers immediately distinguish what is a hardcoded config value from what is a sensitive runtime secret.
Try It: Variables and Types
The sandbox below runs real Python. Explore how Python binds names to objects and how type() reports the type dynamically.
username can be rebound from a string to an integer — Python does not prevent it. Then try adding a new variable that would cause a SyntaxError by starting with a digit, and see the error message Python gives you.
▶ Need help? Show me the code
2fa_token = "abc"Python will raise a
SyntaxError: invalid decimal literal. Variable names cannot start with a digit — Python rejects the line before it even tries to run it.
Try It: Scope in Action
This sandbox lets you observe local and global scope directly. Run it, then try to access a local variable from outside the function to see what Python reports.
attempt_count. You will get a NameError — the local variable does not exist outside the function. This is scope working correctly as a boundary.
▶ Show me
print(attempt_count)Python will raise
NameError: name 'attempt_count' is not defined. The variable only existed inside the function while it was running. Once the function returned, that local scope was discarded.
A developer writes SECRET_KEY = "prod-secret-xyz" at the top of their Django settings file and commits it to a private GitHub repository. The repository is later made public by mistake. What is true?
What You Covered
Variables are name bindings, not containers — a name points to an object in memory.
Dynamic typing means any name can be rebound to any type at any time — intentional or not.
snake_case for variables and functions, ALL_CAPS for constants, PascalCase for classes.
Descriptive names are a security practice — ambiguous names make auditing and code review harder.
LEGB scope determines where a name is visible — keeping sensitive data as local as possible reduces exposure.
Never hardcode secrets into variables — use os.getenv() and fail loudly if the value is missing.
Frequently Asked Questions
Can I use camelCase if I prefer it?
Technically yes — Python will not raise an error. But PEP 8 exists for a reason: it is the style the entire Python ecosystem expects. If you use camelCase for variables and someone else reads your code, reviews it in a security audit, or contributes to your project, they will immediately notice the deviation. Consistency with the broader community reduces friction and makes code easier to audit.
The exception is when working within a codebase that already uses camelCase throughout — in that case, internal consistency matters more than matching PEP 8. But for new code, start with snake_case.
Does Python actually enforce ALL_CAPS as a constant?
No. Python has no native constant type. ALL_CAPS is purely a convention that signals to other developers — and to yourself — that this value should not be reassigned. Nothing stops you from writing MAX_RETRIES = 99 later in the code.
If you need an actual enforced constant in Python, you can use the typing.Final annotation introduced in Python 3.8, which type checkers like mypy will flag if you attempt to reassign. This is covered in a later lesson on type hints.
What is the difference between a global variable and a module-level constant?
Both live at the top level of a module, outside any function. The difference is intent and naming: a global variable is expected to change during a program's execution, while a module-level constant is expected to remain fixed. The naming convention — lowercase for mutable globals, ALL_CAPS for constants — signals that intent to anyone reading the code.
From a security standpoint, mutable globals that hold sensitive data are a risk: any function in the module can read or overwrite them. If a value must be globally accessible and sensitive, prefer loading it once at startup, storing it in a clearly named variable, and making your functions accept it as a parameter rather than reaching up into global scope themselves.
Is os.getenv safe enough for production secrets?
os.getenv() is the correct approach for keeping secrets out of source code, and it is how most Python applications handle configuration. However, environment variables are not encrypted — they are readable by any process running as the same user, and they are often logged in deployment pipelines if you are not careful.
For production systems handling sensitive secrets — API keys, database passwords, cryptographic keys — a dedicated secrets manager such as AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault provides rotation, auditing, and access control that environment variables alone do not. os.getenv() is the right starting point; secrets managers are the production standard. That distinction is covered in a later lesson.
What happens if two variables point to the same mutable object?
Both names see the change. This is called aliasing. Strings and integers are immutable — rebinding one name does not affect the other. Lists and dictionaries are mutable — mutating the object through one name mutates it for all names pointing to the same object.
This matters for security when a data structure holding sensitive values is passed to a function. If the function mutates it — adding, removing, or changing keys — the caller's copy is also changed, because there is only one object. If you need to pass sensitive data to a function without risking mutation, pass a copy: dict.copy() for a shallow copy, or copy.deepcopy() for nested structures. This is introduced in the data structures lesson.
Answer all five questions. You need four correct answers to pass. Questions are shuffled on every attempt. On passing, you can download a certificate of completion with your name on it.
Complete all four sections above before entering your name to begin the exam.