Skip to content

Variables and types

Every program needs to remember things. A quiz needs the player's name. A game needs the current score. A weather script needs the city you're checking. Python uses variables for this: names you attach to values so you can use them throughout your program.

Variables are named references to values. Python binds a name to an object on the right side of = and lets you rebind it at any time. The type lives on the value, not the name.

In Python, a variable is a name binding: a reference in a namespace that resolves to an object at runtime. Objects carry types; names do not. This is dynamic typing: the same name can reference objects of entirely different types across different statements.

python
player_name = "Alice"
score       = 0
city        = "Tokyo"

Three lines. Three things Python now remembers. Use any of those names later and Python hands you back the value.

Each line creates a binding: the name on the left refers to the object on the right. Python evaluates the right side first, then creates the binding.

Each assignment creates or rebinds a name in the current scope's local namespace. Python evaluates the right-hand expression fully before the binding takes effect. The name carries no type information of its own.

Storing a value

The = sign trips up almost everyone coming from maths class. In Python, = does not mean "equals". It means store this value under this name. Read it left to right:

python
city = "Tokyo"

city gets "Tokyo". You're telling Python: remember "Tokyo" and label it city.

You can replace a variable's value at any time. Python just uses the most recent one:

python
score = 0
score = 10   # score is now 10
score = 15   # score is now 15

= is assignment: it binds a name to an object in the current scope. A standard shorthand for updating a variable is augmented assignment:

python
score = 0
score += 10   # same as: score = score + 10
score *= 2    # same as: score = score * 2

You can also bind multiple names at once:

python
x, y, z = 1, 2, 3
a = b = 0        # both start at zero

Assignment binds a name to an object; it does not copy a value into a container. Two names can reference the same object:

python
a = "hello"
b = a
print(id(a) == id(b))   # True (same object in memory)

b = "world"             # b rebound to a new object
print(id(a) == id(b))   # False
print(a)                # still "hello": rebinding b did not affect a

id() returns the object's identity (its memory address in CPython). This distinction between name binding and copying matters more with mutable objects like lists and dicts, covered in later chapters.

Type annotations document expected types for static analysis tools. They have no runtime effect:

python
name:  str   = "Alice"
score: int   = 0
ratio: float = 0.85

Naming your variables

You choose the name. Python has a few hard rules, and the community follows conventions worth adopting from day one. Clear names make code readable weeks later. Cryptic names cause pain.

Python enforces a small set of identifier syntax rules. Beyond those, PEP 8 conventions are the de facto standard across every Python codebase and tool.

Python's identifier syntax rules are minimal. PEP 8 conventions are not enforced by the interpreter but are assumed by linters, type checkers, and every professional Python codebase. Deviating from them creates friction.

Rules Python enforces:

  • Letters, digits, and underscores only. No spaces or hyphens.
  • Must start with a letter or underscore, never a digit
  • Case-sensitive: score, Score, and SCORE are three separate variables

Conventions everyone follows (PEP 8):

ThingStyleExample
Variables and functionssnake_caseuser_name, total_price
ConstantsUPPER_SNAKE_CASEMAX_RETRIES, BASE_URL
ClassesPascalCaseUserAccount, DataLoader
python
# clear names, readable at a glance
user_name    = "Alice"
total_price  = 49.99
is_logged_in = True
MAX_RETRIES  = 3

# you'll regret these within an hour
x   = "Alice"
tp  = 49.99
b   = True

One trap worth knowing early: do not name a variable after a Python built-in like list, input, type, or print. Python allows it, but you will silently break the built-in for the rest of that scope and the resulting errors are hard to trace.

Do not shadow Python's built-ins. Assigning to list, type, input, print, or str overwrites the built-in for the rest of that scope without any warning. It is a silent bug that can be painful to find.

UPPER_SNAKE_CASE is a convention, not enforced. Python will not stop you from reassigning MAX_RETRIES = 99 later. It is a signal to other developers, nothing more.

Shadowing a built-in creates a local binding that takes precedence over the built-in in normal name lookup order. The built-in is still accessible via builtins.print and so on, but the shadowing name hides it in normal use. UPPER_SNAKE_CASE has no language-level enforcement. For genuine immutability guarantees that tools can check, typing.Final in an annotation is the standard approach.

What you can store

Python has four types you will use in almost every program. Python figures out which type you mean from how you write the value. You never declare a type explicitly.

Python infers the type from the literal syntax. These four types cover the fundamental value space; everything else in the language builds on top of them.

Python's four primitive types each map to distinct runtime objects with different memory layouts, precision characteristics, and operation semantics. The type is determined by the object, not the name.

Text (str)

Any text goes inside quote marks, single or double. The quotes tell Python you mean literal characters, not a variable name. Once created, a string cannot be changed in place. The Strings chapter covers everything you can do with them.

python
player_name = "Alice"
city        = "Tokyo"
message     = 'Game over'

If your text contains an apostrophe, use double quotes to avoid having to escape it:

python
note = "It's a great day"
note = 'It\'s a great day'   # same result, using an escape

Strings hold any text in single or double quotes. They are immutable: no operation modifies a string in place; every transformation returns a new one. This matters for performance: repeated + inside a loop creates a new string object on every step. The Strings chapter covers the efficient alternative.

python
player_name = "Alice"
city        = "Tokyo"
note        = "It's a great day"

str is an immutable sequence of Unicode code points, not bytes. len("café") is 4, not 5. Immutability makes strings hashable: valid as dict keys and set members. CPython interns short strings that look like identifiers; two variables assigned the same short literal often share a single object in memory. Both quote styles produce identical objects.

python
player_name = "Alice"
city        = "Tokyo"
note        = "It's a great day"

Whole numbers (int)

Whole numbers go in without quotes or a decimal point. Python calls them integers. They can be as large as you need; Python handles arbitrarily big numbers without any special effort on your part.

python
score      = 0
age        = 28
population = 8_100_000_000   # underscores are just for readability

Integers are written without quotes or decimal points. Python integers are arbitrary precision: they grow to hold any value, unlike the 32- or 64-bit fixed-size integers in C or Java. Underscores in numeric literals are cosmetic and ignored by Python.

python
score      = 0
age        = 28
population = 8_100_000_000

int in Python is arbitrary-precision: the object allocates additional memory as the value grows, bounded only by available RAM. CPython caches small integers from -5 to 256 as singletons; id(1) == id(1) is always True. Outside that range, each literal creates a distinct object. This is why is gives unreliable results for integer comparisons; always use ==.

python
score      = 0
age        = 28
population = 8_100_000_000

Decimal numbers (float)

Any number with a decimal point is a float. They work as expected for most calculations. One thing to know: some decimal values cannot be stored exactly in binary, so you can get a tiny rounding error:

python
price       = 4.99
temperature = 36.6

0.1 + 0.2   # 0.30000000000000004

For everyday work this rarely matters. For financial calculations where fractions of a cent count, Python has a decimal module that handles it correctly. That is covered in the Numbers chapter.

Any number with a decimal point becomes a float. Python floats are IEEE 754 binary64: 64 bits with roughly 15-17 significant decimal digits of precision. The well-known issue: 0.1 + 0.2 is 0.30000000000000004. Not a Python bug; a consequence of binary representation. For financial calculations where exact decimals matter, Python's decimal module is the right tool, covered in the Numbers chapter.

python
price       = 4.99
temperature = 36.6

float maps to C's double: IEEE 754 binary64, 53-bit mantissa, relative precision of 2^-52 ≈ 2.2e-16. Fractions whose denominators have prime factors other than 2 (like 1/10 = 1/(2×5)) are non-terminating in binary and cannot be stored exactly. For exact decimal arithmetic, Python's decimal.Decimal uses arbitrary-precision base-10. For exact rational arithmetic, fractions.Fraction stores numerator/denominator pairs. Both are in the standard library, covered in the Modules chapter.

python
price       = 4.99
temperature = 36.6

True or False (bool)

Some things are simply on or off. Python uses booleans for this: exactly two values, True and False. They look minor at this stage, but every condition and branch in your program runs on a boolean.

python
is_logged_in = True
has_errors   = False

Python also treats certain values as if they were False when used in a condition: 0, 0.0, "", and None (Python's "no value here") all behave like False. Everything else behaves like True. This becomes useful in the Control flow chapter.

bool holds exactly True or False. It is returned by comparisons and consumed by conditions. Python has a broader set of truthy and falsy values: zero values, empty containers, and None are falsy; everything else is truthy. One useful detail: bool is a subclass of int, so True + True evaluates to 2.

python
is_logged_in = True
has_errors   = False

bool subclasses int. True and False are singletons with integer values 1 and 0 respectively. Falsy values: zero values (0, 0.0), empty sequences and mappings ("", [], (), {}), None, and False. Everything else is truthy. Custom objects control this via __bool__ or __len__. isinstance(True, int) is True, which matters in generic type-checking code.

python
is_logged_in = True
has_errors   = False

Checking and converting types

When you are not sure what type a value is, type() tells you. To check whether a value is a specific type, isinstance() is the more reliable tool:

python
print(type("hello"))   # <class 'str'>
print(type(42))        # <class 'int'>
print(type(3.14))      # <class 'float'>
print(type(True))      # <class 'bool'>

isinstance(42, int)    # True
isinstance("hi", str)  # True

type() returns the exact type of an object. For type checking in your own code, isinstance() is preferred: it handles inheritance, which type() comparisons do not.

python
print(type(42))          # <class 'int'>
isinstance(True, int)    # True   (bool is a subclass of int)
type(True) == int        # False  (exact match only, no subclasses)

type(x) returns the type object for x. isinstance(x, T) walks the MRO (x.__class__.__mro__), handling subclass relationships that type() comparisons miss. The practical case: isinstance(True, int) is True because bool subclasses int; type(True) == int is False because it is an exact identity check. Use isinstance() for type guards in production code.

python
isinstance(True, int)    # True
type(True) == int        # False

Python does not mix types automatically. Concatenating a string and a number raises a TypeError:

python
score = 42
print("Your score is " + score)        # TypeError
print("Your score is " + str(score))   # works

Convert explicitly using the type name as a function:

CallResult
str(42)"42"
int(3.9)3 (truncates, does not round)
float("3.14")3.14
int("3.14")ValueError: cannot convert a decimal string to int directly
int(float("3.14"))3 (convert to float first, then to int)
bool(0) / bool("")False

In practice

All four types working together in a small script. The output lines use f-strings to embed values in text: put f before the opening quote and wrap any variable in {}. Python replaces it with the variable's actual value. You will learn them properly in the next chapter.

python
player_name = "Alice"
level       = 3
accuracy    = 0.94
is_premium  = True

print(f"{player_name} is on level {level} with {accuracy:.0%} accuracy.")
print(f"Premium account: {is_premium}")

The types matter because level + 1 works and player_name + 1 does not. Each variable holds exactly one kind of thing; Python will not silently mix them for you.

A realistic config block with all four types, constants separated from runtime state. The f"..." syntax is an f-string: any expression inside {} is evaluated at runtime and embedded in the output. Covered in full in the Output and input chapter.

python
BASE_URL    = "https://api.example.com"
MAX_RETRIES = 3
DEBUG       = False

user_name     = "Alice"
request_count = 0
last_response = None

request_count += 1
print(f"[{request_count}] {BASE_URL} | debug={DEBUG}")

None is the standard placeholder for "no value yet". Its type is NoneType and it behaves as falsy in conditions. Use it as the default for variables that are not meaningful until later in the program.

The same config with inline type annotations. Annotations are documentation for type checkers and IDEs; they have no runtime effect:

python
BASE_URL:    str  = "https://api.example.com"
MAX_RETRIES: int  = 3
DEBUG:       bool = False

user_name:     str        = "Alice"
request_count: int        = 0
last_response: str | None = None

str | None is the union syntax from Python 3.10: the variable holds either a string or None. In earlier versions, the equivalent is Optional[str] from the typing module. The str | None form is preferred in modern Python when the minimum version allows it.