Skip to content

Variables and types

docs.scrimba.com

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 name that points at an object (an object is any value Python holds in memory, like a string or a number), never a labelled box that holds the value itself. The type belongs to the object, not the name, so one name can point at a str on one line and an int on the next with no error. That freedom is called dynamic typing: nothing about the name limits what it is allowed to point at.

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

Three lines, three things Python now remembers. For each one, Python works out the value on the right first, then stores it under the name on the left. Use that name 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 binds a name in the current scope (the region of code where that name is visible, usually the function or file you are working in). Python always works out the entire right-hand side before it binds anything, which is exactly why a, b = b, a swaps two values in one line: the right side is fully computed first, then both names are pointed at the results.

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:

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 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 points a name at an object; it never copies the value into a new container. So two names can end up pointing at the very same object:

python
a = "hello"
b = a
print(id(a) == id(b))   # True (both names point at one object)

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 an object's identity, a number unique to that one object while it exists, so two equal ids mean "literally the same object", not two objects that happen to be equal. The difference between pointing and copying is tempting to ignore now, but it bites with mutable objects (ones you can change in place, like lists): point two names at one list, change it through either name, and the change shows up in both. Lists and dicts make this concrete in later chapters.

A type annotation records the type you expect a name to hold. It is a note for tools that read your code without running it (type checkers and your editor), and Python itself ignores it while the program runs:

python
name: str = "Alice"
score: int = 0
ratio: float = 0.85
JunoStoring a value= isn't maths-class "equals", it's "stash this value under this name". Change it whenever you like and Python keeps the newest. No declaring, no setup, you name it and it exists. Took me way too long to stop overthinking this one.
JunoStoring a value= points a name at a value, and you can re-point it any time. The name is the label, the value is the thing. That's the whole trick.
JunoStoring a value= doesn't drop a value in a box, it ties a name to an object. Reassigning only moves the name, the old object stays put. Remember that before lists show up, or you'll have two names quietly sharing one.

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.

The rules Python actually enforces for an identifier (the technical word for a name) are minimal. The rest is PEP 8, Python's official style guide. The interpreter (the program that runs your code) does not enforce it, but linters (tools that flag style problems and likely bugs), type checkers, and every professional codebase expect it. Going against it mostly creates friction for whoever reads your code next.

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 means making your own name that matches one Python already provides (like list). When Python looks up a name, it checks your local names before its own built-ins, so your version wins and the real list is hidden from that point on. It is still reachable as builtins.list, but ordinary code no longer sees it. As for constants, UPPER_SNAKE_CASE is only a visual signal; Python will happily let you reassign it. If you want a constant a tool can actually enforce, annotate it with typing.Final, which marks the name as "not meant to be reassigned" so type checkers will flag it if you do.

JunoNaming your variables Letters, numbers, underscores, and start with a letter or underscore. Stick to snake_case and you're golden. My one rookie mistake: don't call a variable list or print. Python lets you, then everything goes weird later with zero warning.
JunoNaming your variablessnake_case for names, UPPER_SNAKE_CASE for constants, every tool expects it. And don't name things list or print, you'll clobber the built-in silently.
JunoNaming your variables The interpreter barely enforces naming, but every linter does, so fighting snake_case only earns you grief. The real trap is shadowing a built-in like list: it breaks quietly, and far from where you did it.

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.

Every value in Python is a full object, even a literal (a value written straight into the code, like 42 or "hi"). Being an object means the value carries its own methods (functions attached to it), so "hi".upper() and (3).bit_length() work directly on the literal, with nothing to wrap or unwrap first. You rarely have to think about it, and that is the point. The four types below are the ones you reach for in almost every program.

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"

A str is an immutable sequence of Unicode code points (immutable means it can never be changed after it is created; code points are the characters themselves, not the raw bytes they get stored as), which is why len("café") is 4, not 5. Because a string can't change, it is hashable and can be used as a dictionary key or set member (a value that never changes can be safely filed by its contents). One rule you will actually use falls out of this: compare strings with == (same characters), never is (the same object in memory), because whether two equal strings share one object is an implementation detail you can't rely on.

python
player_name = "Alice"
city = "Tokyo"
note = "It's a great day"
JunoText (str) Text goes in quotes, single or double, your pick. Once it exists you can't change it in place, and that's fine. Anything that looks like it edits a string actually hands you back a brand-new one.
JunoText (str) Strings are immutable. Nothing edits them in place, you always get a new string back. Keep that in mind the moment you start chaining methods.
JunoText (str) An immutable run of Unicode, so len("café") is 4 and strings work as dict keys. The bit you'll actually use: compare with ==, never is.

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 only 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

Two things worth keeping. First, Python's int is arbitrary-precision: it grows to hold any whole number your RAM can fit, so there is no overflow (the wrap-around or error you hit in languages with fixed-size integers) to design around. Second, compare numbers with == (equal value), never is (the same object). Whether two equal ints are the same object is an implementation detail: CPython (the standard Python) reuses small integers, so id(1) == id(1) is True, but that quietly stops holding for larger numbers, so never build on it.

python
score = 0
age = 28
population = 8_100_000_000
JunoWhole numbers (int) Whole numbers, no quotes, no decimal point. Best part, Python ints grow as big as you want with no weird overflow. Those underscores in 8_100_000_000 are only there so you can read it.
JunoWhole numbers (int) Ints are arbitrary-precision, so no overflow to worry about. Underscores in numbers are cosmetic, use them on anything with a lot of digits.
JunoWhole numbers (int) Ints grow till you run out of RAM, so overflow is a non-issue. One habit worth keeping: compare numbers with ==, not is. Identity is a caching quirk, don't lean on it.

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 double-precision: about 15 to 17 significant digits, stored in binary. That binary storage is the well-known catch: 0.1 + 0.2 is 0.30000000000000004, not a Python bug but a consequence of how binary represents decimals. For money, or anywhere exact decimals matter, reach for Python's decimal module, covered in the Numbers chapter.

python
price = 4.99
temperature = 36.6

Floats are stored in binary (base 2), and most decimal fractions, anything whose denominator isn't a power of two (like 1/10), can't be written exactly in binary. That is where 0.1 + 0.2 coming out as 0.30000000000000004 comes from. The rule that matters in production: never use a float for money, or for anything you will check for exact equality. Reach for decimal.Decimal when you need exact base-10 arithmetic, or fractions.Fraction for exact ratios. Both come with Python's standard library (the tools bundled with Python itself), covered in the Modules chapter.

python
price = 4.99
temperature = 36.6
JunoDecimal numbers (float) A decimal point makes it a float, and they're fine for everyday stuff. The classic gotcha everyone hits once: 0.1 + 0.2 gives 0.30000000000000004. Not a bug, that's binary. For money, reach for decimal.Decimal.
JunoDecimal numbers (float) Floats are binary, so some decimals can't be exact, 0.1 + 0.2 isn't quite 0.3. Fine for most maths, never for money. Use decimal.Decimal when it has to be exact.
JunoDecimal numbers (float) Floats drift, that's why 0.1 + 0.2 looks broken. The rule: no floats for money or exact-equality checks. decimal.Decimal for exact base-10, fractions.Fraction for ratios.

True or False (bool)

Some things are 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 is built on top of int (it subclasses it, meaning it is a specialised kind of int), and True and False are the only two bool objects that ever exist, worth exactly 1 and 0. In a condition, the falsy values (the ones that count as false) are: zeros (0, 0.0), empty containers ("", [], (), {}), None, and False itself. Everything else is truthy. Your own classes can decide their truthiness by defining __bool__ or __len__ (the special "dunder" methods, named with double underscores, that Python calls when it needs a yes-or-no answer). And because a bool is an int, isinstance(True, int) is True, which can catch you out in code that checks types.

python
is_logged_in = True
has_errors = False
JunoTrue or False (bool) Two values, True and False, and they're behind every if you'll ever write. The fun bit: 0, "", [] and None all count as False, everything else counts as True.
JunoTrue or False (bool)bool is True/False from comparisons. Python also has truthy and falsy: empty stuff, zero and None are falsy. That's why if my_list: reads so nicely.
JunoTrue or False (bool)bool is secretly an int, so isinstance(True, int) is True, which will surprise you in a type check one day. Falsy is zeros, empties and None. Custom objects pick their own truthiness with __bool__ or __len__.

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) gives you the exact type of x. isinstance(x, T) does more: it walks the MRO (the method resolution order, the ordered list of classes Python searches up the inheritance chain, the line of parent types a class is built from, exposed as x.__class__.__mro__), so it also returns True for parent types. That is why isinstance(True, int) is True (a bool is a kind of int) while type(True) == int is False (an exact-match check). In real code, reach for isinstance() as your type guard, the check that confirms a value is the type you expect before you use it.

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
JunoChecking and converting typestype() tells you what something is, isinstance() checks if it's a given type. And Python won't glue a string and a number together, so convert first with str() or int(). Everyone trips on that once.
JunoChecking and converting types Reach for isinstance() over type() ==, it respects inheritance. Conversions are always explicit, so convert before you concatenate a string and a number.
JunoChecking and converting typesisinstance() is your type guard, it follows the class tree where type(x) == T only matches exactly. Conversions stay explicit on purpose, int("3.14") would rather raise than guess, so go through float first.

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, now with inline type annotations. An annotation records the type you expect a name to hold. It exists for type checkers and your editor (the IDE), and Python ignores it while the program runs:

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 a union type, added in Python 3.10: it says the value is either a string or None. On older versions you write the same thing as Optional[str], imported from the built-in typing module. The str | None form is the one to prefer in modern Python whenever your minimum version allows it.