Why this matters
A program spends its life creating data, inspecting it and combining it. In Python every piece of data, from the number 3 to a function to a module, is an object, and every object has a type that decides what you can do with it. Once you see values as objects with a type, error messages like TypeError: unsupported operand type(s) for +: 'int' and 'str' stop being mysterious: Python is telling you that these two objects' types do not know how to combine. This mental model is the foundation for the whole track.
What an object is
When CPython runs x = 42, it needs the number 42 to exist somewhere in memory. It is stored as an object: a block of memory (a C structure inside the interpreter) holding
- a type: a pointer to the object that describes what kind of thing this is (here,
int), - a reference count: how many references currently point to it (used to free memory; covered in the internals module),
- the value: the actual data, here the digits of 42.
From the Python side you see three properties:
| Property | Question it answers | How to see it | Can it change? |
|---|---|---|---|
| identity | which object is this? | id(obj) | never, for the object's lifetime |
| type | what kind of object? | type(obj) | no |
| value | what data does it hold? | printing it, using it | only for mutable types |
>>> x = 42
>>> type(x)
<class 'int'>
>>> id(x)
140245839127632
The id is a number unique among objects alive at the same time. In CPython it happens to be the memory address of the object. The exact number differs every run, so never rely on it in output; you will use it in the next lesson to tell objects apart.
Literals: how values get into a program
A literal is a value written directly in the source code. The way you write it decides its type:
42 # int: a whole number
-7 # an int too (the minus is an operator applied to 7)
3.14 # float: a number with a fractional part
1e6 # float: 1 times 10 to the 6th, i.e. 1000000.0
"hello" # str: text
'hello' # the same str; quote style does not matter
True # bool: one of the two truth values
None # NoneType: "no value"
[1, 2, 3] # list (module 7)
When the compiler meets a literal, it creates the object and stores it as a constant in the bytecode. Remember LOAD_CONST from the bytecode lesson: that instruction pushes one of these pre-built constant objects.
type() and the built-in types
type(obj) returns the object's type. The core built-in types you will use constantly:
print(type(42))
print(type(3.0))
print(type("3"))
print(type(True))
print(type(None))
<class 'int'>
<class 'float'>
<class 'str'>
<class 'bool'>
<class 'NoneType'>
<class 'int'> is the repr of the type object. If you want just the name, every class has a __name__ attribute:
print(type(42).__name__)
int
The dot in type(42).__name__ is attribute access: "get the attribute called __name__ from this object". Names surrounded by double underscores ("dunder" names) are special names that Python itself defines and uses.
Types decide behavior
The same operator means different things for different types, because each type defines what its operations do:
print(3 + 4) # int + int: addition
print("3" + "4") # str + str: concatenation
print("ab" * 3) # str * int: repetition
print(3 * 4) # int * int: multiplication
7
34
ababab
12
When you write a + b, the VM looks at the type of a and asks it how to add b (concretely, it calls the type's __add__ method; you will write such methods yourself in the object-oriented module). If neither type knows how, you get a TypeError:
print(3 + "4")
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Python is strongly typed: it does not quietly convert "4" into 4 or 3 into "3" for you. It is also dynamically typed: names do not have types, objects do. The same name can refer to an int now and a string later. Both ideas come up again and again.
Asking "is this a kind of X?": isinstance
type(x) == int checks for exactly int. Usually what you want is "is x an int or something that behaves as one", which is isinstance:
print(isinstance(42, int))
print(isinstance("42", int))
print(isinstance(3.0, (int, float))) # any of several types
True
False
True
The difference matters with subtypes. You will see in lesson 4 that bool is a subtype of int, so isinstance(True, int) is True while type(True) == int is False.
Even types and functions are objects
"Everything is an object" is literal. Functions are objects; types are objects; modules are objects:
print(type(print))
print(type(int))
print(type(type))
<class 'builtin_function_or_method'>
<class 'type'>
<class 'type'>
Because int is an object, you can store it in a variable and call it later: convert = int then convert("5") gives 5. This uniformity is one of Python's great strengths, and later modules (functions as values, decorators, classes) are built on it.
Common mistakes
- Judging type by appearance.
"42"looks like a number but is astr. When in doubt,print(type(x)), orprint(repr(x))to see quotes. - Printing ids in exercises.
id()values change between runs, so they can never be part of expected output. - Calling a type name as if it were a value of that type.
int + 1givesTypeError: unsupported operand type(s) for +: 'type' and 'int':intis the class, not a number. - Shadowing built-ins.
type = "admin"orstr = "hello"rebinds the built-in name in your program; laterstr(5)fails withTypeError: 'str' object is not callable. Avoid naming variableslist,str,type,id,input,sum,max,min.
Recap
- Every value is an object with an identity (
id), a type (type) and a value. - Literals like
42,3.14,"hi",True,Nonecreate objects whose type the compiler reads from the spelling. - The type decides which operations work and what they mean; unsupported combinations raise
TypeError. - Python is strongly typed (no silent conversions) and dynamically typed (objects have types, names do not).
isinstance(x, T)checks "is a kind of"; types and functions are objects too.
Going further: in the REPL, try dir(42) to list the attributes of an int object, and help(int) to read about them. Many have dunder names: that is how operators are implemented.