Python Part I · Module 16 · Object-Oriented Programming
Everything Is an Object: Attributes and Methods
This module introduces classes, and it opens by pointing out that you have been using them since Module 4. Every time you wrote line.strip(), results.get(), path.exists() or f.read(), you were asking a value to do something it knows how to do — and the fact that those four look alike is not a coincidence of syntax but the single most important structural fact about the language. Before writing a class it is worth being precise about what the dot has been doing all along, because once that is clear, the syntax in the next chapter is a way of making your own version of something you already understand.
Intermediate15 min readPythonPart IFundamentalsOOPObjects
Part I · Module 16 · Chapter 16.1 · Everything Is an Object: Attributes and Methods
1. The Dot You Already Use
Four lines from four different modules:
line.strip() # Module 4, a string
results.get(k) # Module 8, a dictionary
f.read() # Module 15, a file
path.exists() # Module 15, a PathFour unrelated kinds of value, one piece of syntax. The pattern is always the same: a value, a dot, and a name that belongs to that value.
What the dot means is: look up this name on this value. What comes back
depends on what the value's type defines, which is why strip works on a
string and not on a dictionary.
2. Everything Means Everything
The claim in the title is literal. Every value you have used in fifteen modules has a type, and every one of them carries that type with it:
VALUES = ["smoke", 42, 3.14, [1, 2], {"a": 1}, (1, 2), {1, 2}, None, True]
for v in VALUES:
print(f"{str(v):<10} type={type(v).__name__:<9} has a class={hasattr(v, '__class__')}")smoke type=str has a class=True
42 type=int has a class=True
3.14 type=float has a class=True
[1, 2] type=list has a class=True
{'a': 1} type=dict has a class=True
(1, 2) type=tuple has a class=True
{1, 2} type=set has a class=True
None type=NoneType has a class=True
True type=bool has a class=TrueNone is an object. So is True. So is the number 42. There is no
category of "plain data" in Python that sits outside this — which is why
type(e).__name__ worked on exceptions in Chapter 14.3 and why .strip() works
on a string.
A class is the thing that decides what names a value responds to. str is a
class; "smoke" is one instance of it. Those two words are the whole
vocabulary.
3. Attribute or Method?
The canonical pair for this chapter:
path.name # an attribute: data it holds
path.exists() # a method: something it can doThe difference is the parentheses, and the underlying difference is what the name refers to:
- an attribute is a value the object holds — you read it;
- a method is a function the object owns — you call it.
Both are looked up the same way. The mechanical test is callable():
from pathlib import Path
p = Path("runs/seed_41/sim.log")
print("attribute name :", p.name)
print("attribute suffix :", p.suffix)
print("method result :", p.is_absolute())
print()
print("is .name callable() :", callable(p.name))
print("is .is_absolute callable() :", callable(p.is_absolute))attribute name : sim.log
attribute suffix : .log
method result : False
is .name callable() : False
is .is_absolute callable() : TrueNote the last line carefully: p.is_absolute has no parentheses. That is
deliberate, and it is the subject of the next section.
Numbers have attributes too, which is a useful shock:
print("real:", (5).real)
print("imag:", (5).imag)
print("numerator:", (5).numerator)
print("callable?", callable((5).real))real: 5
imag: 0
numerator: 5
callable? False4. Forgetting the Parentheses Does Not Raise
The single most common slip with the dot:
s = " smoke "
print("A:", repr(s.strip()))
print("B:", repr(s.strip).split(" of ")[0] + " ...>")
print("C:", type(s.strip).__name__)
print("D:", callable(s.strip))
print("E:", s.strip.__name__)A: 'smoke'
B: <built-in method strip ...>
C: builtin_function_or_method
D: True
E: stripB is not an error. s.strip is a perfectly good value — it is the method
itself, bound to this particular string, waiting to be called. Printing it
shows what it is rather than what it would return.
The full text of B is trimmed here, because the untrimmed version ends in a memory address that differs on every run. That is a real property of how Python displays an object it has not been told how to display, and Chapter 16.8 is about replacing it with something useful.
E shows the method knows its own name, which is occasionally handy when something has handed you a function and you want to know which one.
The practical consequence is that a missing () fails later and strangely:
line = "UVM_ERROR drv.sv(88)\n"
cleaned = line.strip
print("cleaned:", cleaned.upper())Traceback (most recent call last):
File "noparens.py", line 3, in <module>
print("cleaned:", cleaned.upper())
^^^^^^^^^^^^^
AttributeError: 'builtin_function_or_method' object has no attribute 'upper'The error is about builtin_function_or_method, several steps from the missing
parentheses on line 2. This is Chapter 14.2 Section 5 exactly: the error
surfaces where the bad value is used, not where it was created — and the type
name in the message is the clue.
5. Methods Are Functions That Receive the Object
A method differs from a plain function in one respect: the object is handed to it automatically.
word = "smoke"
print("A:", word.upper())
print("B:", str.upper(word))
print("C:", word.upper() == str.upper(word))A: SMOKE
B: SMOKE
C: TrueB is the same call written the long way. word.upper() looks up upper on
the class str and passes word in as the first argument. The dot form is
shorthand for the second.
That is the whole mechanism behind self, which Chapter 16.3 is about. When you
write your own method, the object arrives as the first parameter, because that
is what the dot does.
6. AttributeError Reads Differently Now
Chapter 14.3 Section 8 catalogued AttributeError as "the object has no such
name". With the model in place, the message is more specific than that:
name = "smoke"
name.push("_test")Traceback (most recent call last):
File "attr.py", line 2, in <module>
name.push("_test")
^^^^^^^^^
AttributeError: 'str' object has no attribute 'push'Read it as: the class str defines no name push. The message names the
class first because the class is what decides. push is a list method in some
languages and no Python class has it; append is a list method and str does
not have that either.
Which is why the type in an AttributeError is the first thing to read: it tells
you what Python thinks it is holding, and the surprise is often that it is
holding something other than you believed — most commonly NoneType.
7. Seeing What an Object Offers
dir() lists the names a value responds to:
public = []
for name in dir("smoke"):
if not name.startswith("_"):
public.append(name)
print("str has", len(public), "public names")
print("first eight:", public[:8])
print("strip among them:", "strip" in public)
print("push among them :", "push" in public)str has 47 public names
first eight: ['capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find']
strip among them: True
push among them : FalseThe names beginning with underscores are filtered out here. Those are the
special methods — Chapter 16.8's subject — and they are how len(),
print() and + actually work.
8. Functions and Classes Are Objects Too
Section 2's table covered values. The claim goes further than that, and this is the part that surprises people arriving from C or SystemVerilog:
def count_errors(lines):
"""Count UVM_ERROR lines in a log."""
return 0
print("its type :", type(count_errors).__name__)
print("its name :", count_errors.__name__)
print("its docstring:", count_errors.__doc__)
print("callable :", callable(count_errors))its type : function
its name : count_errors
its docstring: Count UVM_ERROR lines in a log.
callable : TrueA function is an object with attributes. __name__ and __doc__ are not
special syntax — they are attributes the function carries, which is exactly how
help() works and how Chapter 11.11's docstrings become visible to tools.
Being an object means a function can be stored, passed and looked up like any other value:
def count_errors(lines):
return len([l for l in lines if "UVM_ERROR" in l])
def count_warnings(lines):
return len([l for l in lines if "UVM_WARNING" in l])
LOG = ["UVM_ERROR drv.sv(88)", "UVM_WARNING late", "UVM_ERROR mon.sv(12)"]
checkers = {"errors": count_errors, "warnings": count_warnings}
for name in sorted(checkers):
print(f"{name:<9} {checkers[name](LOG)}")errors 2
warnings 1The dictionary holds the functions themselves, not calls to them — note
there are no parentheses in {"errors": count_errors}, which is Section 4's
distinction doing useful work rather than causing a bug. checkers[name](LOG)
looks the function up and then calls it.
That is also the mechanism behind key= in sorted() (Chapter 6.x) and behind
Chapter 11.14's lambdas: both take a function as an ordinary argument.
And classes are objects as well:
print("type of a class :", type(int).__name__)
print("type of type :", type(type).__name__)
print("a class is callable:", callable(int))
print("calling it makes an instance:", int("42"), type(int("42")).__name__)type of a class : type
type of type : type
a class is callable: True
calling it makes an instance: 42 intA class is an object of type type, and calling it is what creates an
instance — which is why TestRun("smoke") in the next chapter looks like a
function call. It is one.
9. Looking a Name Up at Run Time
The dot needs the attribute name written in the source. When the name is in a variable instead, there is a function for it:
class Run:
def __init__(self):
self.name = "smoke"
self.seed = 1234
r = Run()
for field in ["name", "seed", "missing"]:
print(f" {field:<8} {getattr(r, field, '<absent>')}")
setattr(r, "status", "PASS")
print("after setattr:", r.__dict__)
print("hasattr :", hasattr(r, "status"), hasattr(r, "nope")) name smoke
seed 1234
missing <absent>
after setattr: {'name': 'smoke', 'seed': 1234, 'status': 'PASS'}
hasattr : True Falsegetattr(obj, "name") is obj.name with the name as data. The third
argument is a default, which turns a would-be AttributeError into a value —
the same shape as dict.get() from Chapter 8.3.
This is how a generic report writer works: given a list of field names, it can pull each one from any object without knowing the class.
class Run:
def __init__(self, name, seed, runtime_s):
self.name = name
self.seed = seed
self.runtime_s = runtime_s
def as_row(obj, fields):
"""Works on any object that has these attributes."""
return " ".join(str(getattr(obj, f, "-")) for f in fields)
runs = [Run("smoke", 1234, 182.4), Run("cdc", 5678, 204.1)]
for run in runs:
print(as_row(run, ["name", "seed", "runtime_s"]))
print(as_row(runs[0], ["name", "nonexistent"]))smoke 1234 182.4
cdc 5678 204.1
smoke -Use these when the name genuinely comes from data — a config file, a column
list, a command-line flag. Do not use them to avoid writing the dot:
getattr(r, "name") where r.name would do is harder to read and invisible to
every tool that checks your code.
10. Predict, Run, Observe
values = ["smoke", [1, 2], {"a": 1}]
for v in values:
print(f"{type(v).__name__:<5} has strip={hasattr(v, 'strip'):<2} "
f"has append={hasattr(v, 'append'):<2} has get={hasattr(v, 'get')}")
print("---")
d = {"test": "smoke"}
print("A:", callable(d.get))
print("B:", d.get("test"))
print("C:", type(d.get).__name__)
print("D:", hasattr(d, "keys"), callable(d.keys))str has strip=1 has append=0 has get=False
list has strip=0 has append=1 has get=False
dict has strip=0 has append=0 has get=True
---
A: True
B: smoke
C: builtin_function_or_method
D: True TrueThe table is the chapter in one grid. Each class defines its own set of names; there is no universal method list.
The same booleans print two different ways in one line. strip and
append were given a width specifier (:<2) and came out as 1 and 0;
get had none and came out as True and False. A bool is an integer in
Python — True is 1 and False is 0 — and a numeric format specifier makes it
show as one. The values are identical; only the rendering differs.
A, C and D show that d.get without parentheses is a value, and a callable
one. B calls it.
11. Exercises
Exercise 1 — nine types. Reproduce Section 2's table. Add two more values of your own and confirm they also have a class.
Exercise 2 — attribute or method. For a Path, list three attributes and
three methods. Verify each with callable().
Exercise 3 — the missing parentheses. Assign line.strip to a name and use
it as though it were a string. Record the error and say which chapter's
symptom-versus-cause rule applies.
Exercise 4 — the long form. Call three string methods both as x.m() and as
str.m(x). Confirm the results match.
Exercise 5 — read the type. For each error, say what the object actually was:
(a) 'NoneType' object has no attribute 'strip' (b) 'list' object has no attribute 'keys' (c) 'int' object is not subscriptable.
Exercise 6 — dir(). Count the public names on list, dict and set. Name
one that appears on all three and one that appears on only one.
Exercise 7 — the underscore names. Print the names on str that do start
with an underscore. Pick three you recognise and guess what calls them.
Exercise 8 — identify an unknown. Open a file, and without looking anything
up, use type and dir to list five things the handle can do. Say which one
you would reach for to read a single line.
12. Summary
- The dot looks up a name on a value, and what it finds is decided by the value's class.
- Every value is an object — including
None,Trueand42. - A class decides what names a value responds to; an instance is one value of that class.
- An attribute is data you read; a method is a function you call. The test is
callable(). - Omitting the parentheses does not raise — you get the method object itself, and the failure appears later with a confusing type name.
x.method()is shorthand forClass.method(x), which is whereselfcomes from.AttributeErrornames the class first, because the class is what defines the name — and aNoneTypethere means something returnedNone.- Functions are objects carrying
__name__and__doc__, and can be stored in a dict, passed as an argument and looked up by name. - Classes are objects too, of type
type, and calling one creates an instance. dir()lists what an object offers; underscore names are the special methods.getattr,setattrandhasattrtake the name as data, with a default that turns anAttributeErrorinto a value — for names that come from a config or a column list, never to avoid writing the dot.
The next chapter writes a class of your own, which turns out to be a way of deciding what names your values will respond to.
Where this fits
Part of the Python curriculum.
