PythonMastery
beginner 13 min read · lesson 2 of 19 in Python Fundamentals

Variables & Data Types

1 · The lesson

read

In Python, a variable is not a box that holds a value. A variable is a name that refers to an object stored somewhere in memory. This difference may seem small now. It becomes important when two names refer to the same list, which you will see in section 6.

This lesson covers the binding model, the naming conventions most Python programmers follow, the built-in types you will use from the start, and the difference between values you can mutate in place and values you can't.


1. Binding, Not Boxing

python
x = 42

Python creates the integer object 42, then makes the name x refer to it. The name lives in the current scope, and the object lives in memory. If you assign a new value, the name refers to a different object. If nothing else refers to the old object, Python deletes it (this is called garbage collection).

python
x = 42
x = "now I'm a string"          # also allowed, because x is only a label
x = [1, 2, 3]                   # still allowed, because a label has no fixed type

This is dynamic typing: values have types, names don't. The type lives on the object, not the variable.


2. Naming Rules and PEP 8

Python's rules for what's a legal name:

  • Letters, digits, and underscores only
  • Cannot start with a digit
  • Cannot be a reserved keyword (if, for, class, lambda, True, etc.)
  • Case-sensitive: score and Score are different names
python
user_age = 30                   # fine
_internal = "ok"                # a leading underscore means "private" (by convention only)
2nd_place = "bronze"            # SyntaxError: a name can't start with a digit
class = "Physics 101"           # SyntaxError: class is a keyword (a reserved word)

PEP 8 is Python's official style guide. The interpreter does not enforce its conventions, but almost every team expects you to follow them:

KindConventionExample
Variable, functionsnake_caseuser_age, parse_input
ConstantUPPER_SNAKEMAX_RETRIES, PI
ClassPascalCaseHttpClient, User
"Private"leading __internal_cache

Names should describe what the value means, not what type it is. users is better than user_list. count is better than n.


3. The Built-in Types You'll See Every Day

The five primitives:

python
age = 30                        # int     whole numbers of any size
pi = 3.14159                    # float   decimal numbers, accurate to about 15-17 digits
name = "Ada"                    # str     text
is_active = True                # bool    True or False (note the capital letters)
result = None                   # None    "no value" (other languages call this null)

Then the four built-in collections you'll get dedicated lessons on:

python
scores = [88, 92, 75]           # list    ordered, can be changed     (see ../lists/)
point  = (3, 4)                 # tuple   ordered, cannot be changed  (see ../tuples/)
user   = {"name": "Ada"}        # dict    maps keys to values         (see ../dictionaries/)
tags   = {"python", "web"}      # set     unique items, no order      (see ../sets/)

Don't memorise the collections yet. Each one has its own lesson. For now, just know they exist and recognise how each one is written.


4. Inspecting Types: type() and isinstance()

python
print(type(42))                 # <class 'int'>
print(type(3.14))               # <class 'float'>
print(type("hi"))               # <class 'str'>
print(type([1, 2]))             # <class 'list'>

type() is fine for quick debugging. For real checks, especially inside functions, use isinstance(). It also handles inheritance correctly:

python
x = 42
print(isinstance(x, int))       # True
print(isinstance(x, (int, float)))   # True  (isinstance accepts a tuple of allowed types)

You'll use isinstance() again in the Exceptions lesson when validating inputs.


5. Type Conversion

Python won't convert types for you without being asked. You convert a value by calling the type as a function:

python
int("42")                       # 42        (the string "42" becomes the int 42)
float("3.14")                   # 3.14      (the string becomes a float)
str(3.14)                       # "3.14"    (any value can become a string)
bool(0), bool(1), bool("")      # False, True, False

The trap:

python
int("3.14")                     # ValueError: int() won't parse a decimal string

Go through float first:

python
print(int(float("3.14")))       # 3

You will usually meet this problem first when you read numbers typed by a user:

python
age = int(input("Age: "))       # crashes if the user types "thirty"

You'll handle that properly in Errors & Exceptions.


6. Mutability: an Idea You Will Need in Every Later Lesson

Some objects can be changed in place. Some can't. This difference affects how assignment behaves.

Immutable: int, float, str, bool, tuple, None. Once created, the object can never change. Operations return new objects.

python
name = "ada"
name.upper()                    # returns "ADA"  (but name is still "ada")
name = name.upper()             # now name points to the new string "ADA"

Mutable: list, dict, set. The object itself can be modified.

python
scores = [88, 92, 75]
scores.append(100)              # changes the existing list in place
print(scores)                   # [88, 92, 75, 100]

This has a result that surprises almost every beginner once:

python
a = [1, 2, 3]
b = a                           # b is not a copy. It is another name for the same list
b.append(99)
print(a)                        # [1, 2, 3, 99]   (a changed too, which surprises most people)

= doesn't copy. It binds another name to the same object. To make a real copy of a list, use a.copy() or list(a). We'll come back to this in Lists.


Common Mistakes

  • Shadowing built-ins. Naming a variable list, dict, str, or type hides the built-in for the rest of that scope. If you write list = [1, 2, 3], then list(range(5)) fails with a TypeError. Pick items or values instead.
  • Assuming = copies. It doesn't. Both names point to the same object. This only matters for mutable types, but for them it matters a lot.
  • int(input(...)) with no error handling. This is fine in a tutorial, but not in a program that real people will use. Wrap it in try/except once you've read the exceptions lesson.
  • Confusing None with False or 0. They're all "falsy" (see Operators), but they're distinct objects. None means "no value yet", 0 is a number, and False is a boolean.

🎯 Your Turn: Describe Any Value

Write a short program that prints three things about the value in the variable value:

1. The type of the value (e.g. int, str, list).
2. The value itself.
3. Its length.

With value = "Margaret", it should print the type str, the value Margaret, and the length 8. Then change the first line to value = [1, 2, 3, 4] and run it again.

Skeleton:

python
value = "Margaret"

# TODO 1: print the type
# TODO 2: print the value
# TODO 3: print the length
Hint 1: Getting a clean type name type(value).__name__ gives you the short string "int" instead of <class 'int'>. This is easier to read when you print it.
Hint 2: Printing a label and a value print() accepts several values separated by commas, and puts a space between them: print("Length:", len(value)) prints Length: 8.
Show full solution
python
value = "Margaret"

print("Type:", type(value).__name__)
print("Value:", value)
print("Length:", len(value))

Output:

python
Type: str
Value: Margaret
Length: 8
+ setup added so this can run · defines Margaret
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

Margaret = _AutoMock('Margaret')

With value = [1, 2, 3, 4], the output is Type: list, Value: [1, 2, 3, 4] and Length: 4.

Now try value = 42. The first two lines print, and then len(value) raises TypeError: object of type 'int' has no len(), because a number has no length. In the Conditionals lesson you will learn if, which lets a program skip the length line for values like this.


What You Learned

  • A variable is a name bound to an object, not a container. Names have no type, but objects have the type.
  • PEP 8: snake_case for variables and functions, UPPER_SNAKE for constants, PascalCase for classes.
  • The five primitive types (int, float, str, bool, None) and the four collections you'll meet later.
  • type() for debugging, isinstance() for real checks.
  • Conversion is explicit: int("42"), str(3.14). int("3.14") raises a ValueError, so convert with float first.
  • Mutability matters. = never copies. It only binds another name to the same object.

Next: Numbers in Depth: integer division, floating-point rounding errors and the math module.

Practice this

on practicepython.in

Short exercises that run in your browser and tell you what your code did, as well as whether a test passed.

Found this useful? Share it with a friend who’s learning Python.