{
const scripts = Array.from(
document.querySelectorAll("script.fcpython-ojs-quiz-config")
);
const script = scripts.find(
(node) => node.dataset.fcpythonRendered !== "true"
);
if (!script) {
return html`<div class="fcpython-quiz fcpython-quiz-warning">
Quiz configuration was not found.
</div>`;
}
script.dataset.fcpythonRendered = "true";
const quiz = JSON.parse(script.textContent);
const container = html`<div class="fcpython-quiz"></div>`;
const title = document.createElement("h3");
title.textContent = quiz.title;
container.appendChild(title);
const instructions = document.createElement("p");
instructions.textContent = quiz.instructions;
container.appendChild(instructions);
const progress = document.createElement("div");
progress.className = "fcpython-quiz-progress";
const counter = document.createElement("p");
counter.className = "fcpython-quiz-counter";
counter.setAttribute("aria-live", "polite");
progress.appendChild(counter);
const tabs = document.createElement("div");
tabs.className = "fcpython-quiz-steps";
tabs.setAttribute("role", "tablist");
tabs.setAttribute("aria-label", "Quiz questions");
progress.appendChild(tabs);
container.appendChild(progress);
const questions = document.createElement("div");
questions.className = "fcpython-quiz-questions";
const feedbackNodes = [];
const questionPanels = [];
const stepButtons = [];
let currentQuestion = 0;
function setStepStatus(questionIndex, status) {
const step = stepButtons[questionIndex];
const question = quiz.questions[questionIndex];
const statuses = {
answered: "answered",
correct: "correct",
incorrect: "incorrect",
unanswered: "not answered",
};
step.classList.remove(
"is-answered",
"is-correct",
"is-incorrect",
"is-unanswered"
);
if (status) {
step.classList.add(`is-${status}`);
}
const statusLabel = status ? `, ${statuses[status]}` : "";
step.setAttribute(
"aria-label",
`Question ${questionIndex + 1}: ${question.prompt}${statusLabel}`
);
}
function showQuestion(questionIndex, focusPanel = false) {
currentQuestion = Math.max(
0,
Math.min(questionIndex, quiz.questions.length - 1)
);
questionPanels.forEach((panel, index) => {
panel.hidden = index !== currentQuestion;
});
stepButtons.forEach((step, index) => {
const isCurrent = index === currentQuestion;
step.classList.toggle("is-current", isCurrent);
step.setAttribute("aria-selected", String(isCurrent));
step.tabIndex = isCurrent ? 0 : -1;
});
counter.textContent = `Question ${currentQuestion + 1} of ${quiz.questions.length}`;
const selected = questionPanels[currentQuestion].querySelector(
"input[type='radio']:checked"
);
const isLastQuestion = currentQuestion === quiz.questions.length - 1;
previous.hidden = currentQuestion === 0;
next.hidden = isLastQuestion || !selected;
check.hidden = !isLastQuestion;
if (focusPanel) {
questionPanels[currentQuestion].focus();
}
}
quiz.questions.forEach((question, questionIndex) => {
const tabId = `${quiz.id}-question-tab-${questionIndex + 1}`;
const panelId = `${quiz.id}-question-panel-${questionIndex + 1}`;
const step = document.createElement("button");
step.type = "button";
step.className = "fcpython-quiz-step";
step.id = tabId;
step.textContent = String(questionIndex + 1);
step.setAttribute("role", "tab");
step.setAttribute("aria-controls", panelId);
step.setAttribute("aria-selected", "false");
step.tabIndex = -1;
step.addEventListener("click", () => showQuestion(questionIndex));
step.addEventListener("keydown", (event) => {
let destination = null;
if (event.key === "ArrowRight") {
destination = (questionIndex + 1) % quiz.questions.length;
} else if (event.key === "ArrowLeft") {
destination =
(questionIndex - 1 + quiz.questions.length) % quiz.questions.length;
} else if (event.key === "Home") {
destination = 0;
} else if (event.key === "End") {
destination = quiz.questions.length - 1;
}
if (destination !== null) {
event.preventDefault();
showQuestion(destination);
stepButtons[destination].focus();
}
});
stepButtons.push(step);
tabs.appendChild(step);
setStepStatus(questionIndex, "");
const panel = document.createElement("div");
panel.className = "fcpython-quiz-panel";
panel.id = panelId;
panel.setAttribute("role", "tabpanel");
panel.setAttribute("aria-labelledby", tabId);
panel.tabIndex = -1;
const fieldset = document.createElement("fieldset");
fieldset.className = "fcpython-quiz-question";
const legend = document.createElement("legend");
legend.textContent = question.prompt;
fieldset.appendChild(legend);
question.options.forEach((option, optionIndex) => {
const label = document.createElement("label");
label.className = "fcpython-quiz-option";
const input = document.createElement("input");
input.type = "radio";
input.name = `${quiz.id}-${question.id}`;
input.value = String(optionIndex);
input.addEventListener("change", () => {
setStepStatus(questionIndex, "answered");
feedbackNodes[questionIndex].textContent = "";
feedbackNodes[questionIndex].className = "fcpython-quiz-feedback";
score.textContent = "";
showQuestion(questionIndex);
});
const text = document.createElement("span");
text.textContent = option;
label.appendChild(input);
label.appendChild(text);
fieldset.appendChild(label);
});
const feedback = document.createElement("p");
feedback.className = "fcpython-quiz-feedback";
feedback.setAttribute("aria-live", "polite");
feedbackNodes.push(feedback);
fieldset.appendChild(feedback);
panel.appendChild(fieldset);
questionPanels.push(panel);
questions.appendChild(panel);
});
container.appendChild(questions);
const actions = document.createElement("div");
actions.className = "fcpython-quiz-actions";
const previous = document.createElement("button");
previous.type = "button";
previous.className = "fcpython-quiz-secondary";
previous.textContent = "Previous";
previous.addEventListener("click", () => {
showQuestion(currentQuestion - 1, true);
});
const next = document.createElement("button");
next.type = "button";
next.textContent = "Next question";
next.addEventListener("click", () => {
showQuestion(currentQuestion + 1, true);
});
const check = document.createElement("button");
check.type = "button";
check.textContent = "Check answers";
const reset = document.createElement("button");
reset.type = "button";
reset.className = "fcpython-quiz-secondary fcpython-quiz-reset";
reset.textContent = "Reset";
const score = document.createElement("p");
score.className = "fcpython-quiz-score";
score.setAttribute("aria-live", "polite");
check.addEventListener("click", () => {
let correctCount = 0;
let firstQuestionToReview = null;
quiz.questions.forEach((question, questionIndex) => {
const selected = container.querySelector(
`input[name="${quiz.id}-${question.id}"]:checked`
);
const feedback = feedbackNodes[questionIndex];
if (!selected) {
feedback.textContent = "Choose an answer before checking.";
feedback.className = "fcpython-quiz-feedback";
setStepStatus(questionIndex, "unanswered");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
return;
}
const selectedIndex = Number(selected.value);
if (selectedIndex === question.answer_index) {
correctCount += 1;
feedback.textContent = `✅ Correct. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-correct";
setStepStatus(questionIndex, "correct");
} else {
const answer = question.options[question.answer_index];
feedback.textContent = `❌ Not yet. Correct answer: ${answer}. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-incorrect";
setStepStatus(questionIndex, "incorrect");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
}
});
score.textContent = `Score: ${correctCount}/${quiz.questions.length}`;
if (firstQuestionToReview !== null) {
showQuestion(firstQuestionToReview, true);
}
});
reset.addEventListener("click", () => {
container.querySelectorAll("input[type='radio']").forEach((input) => {
input.checked = false;
});
feedbackNodes.forEach((feedback) => {
feedback.textContent = "";
feedback.className = "fcpython-quiz-feedback";
});
stepButtons.forEach((_, questionIndex) => {
setStepStatus(questionIndex, "");
});
score.textContent = "";
showQuestion(0, true);
});
actions.appendChild(previous);
actions.appendChild(next);
actions.appendChild(check);
actions.appendChild(reset);
container.appendChild(actions);
container.appendChild(score);
showQuestion(0);
return container;
}
FreeCampus Python
Build Your First Useful Class
Refactor a working dictionary into a class, create independent instances, inspect instance and class state, and trace how Python binds self to method calls.
python-foundations
object-oriented-python
classes
instances
methods
Course progress
0%
An expedition team records its trail name, distance, and visited checkpoints. A dictionary and functions can solve that task well. We will begin there—not because the code is wrong, but because a useful class should improve a real design rather than appear only to demonstrate syntax.
As you work, answer these questions:
- What becomes easier when one object owns both state and valid operations?
- Where does Python store an instance’s attributes?
- Why do two instances of the same class change independently?
- How does
tracker.record_distance(3)supplyself? - When does a class attribute help, and when does it accidentally share state?
1. Start with a dictionary that works
The first version uses a dictionary for state and functions for behavior:
def create_expedition(name):
"""Return a new expedition record."""
return {
"name": name,
"distance_km": 0.0,
"checkpoints": [],
}
def record_distance(expedition, distance_km):
"""Add a non-negative distance to an expedition."""
if distance_km < 0:
raise ValueError("distance_km cannot be negative")
expedition["distance_km"] += distance_km
def visit_checkpoint(expedition, checkpoint):
"""Record a non-empty checkpoint name."""
cleaned = checkpoint.strip()
if not cleaned:
raise ValueError("checkpoint cannot be blank")
expedition["checkpoints"].append(cleaned)Create two expeditions and use the functions:
The output shows that the dictionaries are independent:
This is valid procedural Python. Keep it when the data shape is small, the operations are few, and passing a record explicitly remains clear.
Now imagine ten functions spread across several modules. Every caller must know the exact key spelling. Any code can assign dawn["distance_km"] = -900. A checkpoint function could accidentally receive a customer dictionary with similar keys. The design lacks a clear owner, even though every individual line is legal.
NoteA class is not automatically shorter
The class version may contain more lines. Its value is a named kind of object, a clear call interface, and one place to keep related rules—not line-count reduction.
2. Move cohesive state and behavior together
Define a class with the class statement:
class ExpeditionTracker:
"""Track the progress of one expedition."""
def __init__(self, name):
self.name = name
self.distance_km = 0.0
self.checkpoints = []
def record_distance(self, distance_km):
if distance_km < 0:
raise ValueError("distance_km cannot be negative")
self.distance_km += distance_km
def visit(self, checkpoint):
cleaned = checkpoint.strip()
if not cleaned:
raise ValueError("checkpoint cannot be blank")
self.checkpoints.append(cleaned)Read the definition from the outside inward:
class ExpeditionTracker:asks Python to execute a class body and bind the resulting class object to the nameExpeditionTracker.- Each
defcreates a function in the class namespace. __init__describes how to initialize one new instance.- Assignments such as
self.name = namestore data on that instance. - Later methods read or change the instance received as
self.
Calling the class produces instances:
dawn and dusk are both ExpeditionTracker instances, but they are not the same object. Each call created a separate instance and __init__ assigned a new checkpoint list to it.
type(dawn) identifies the class that created the instance. isinstance(dawn, ExpeditionTracker) asks whether dawn is an instance of that class or one of its subclasses. The is expression asks the stricter identity question from Unit 6.
Checkpoint: instances own independent state
3. See what initialization does—and does not do
It is common to say “__init__ creates the object,” but that shortcut causes confusion later. A more accurate beginner model is:
- calling
ExpeditionTracker("Dawn Trail")starts Python’s instance construction machinery; - Python obtains a new instance;
- Python passes that instance to
__init__asselfalong with the supplied arguments; and - the class call returns the initialized instance.
__new__ participates in creating the instance, but customizing it is outside this foundation unit. The important correction is that __init__ initializes an instance and must return None.
This mistake is legal to write but fails when the class is called:
Python reports that __init__() should return None, not a BrokenTracker. Remove the return self. The class call already returns the instance.
Inspect the instance namespace
Most ordinary instances keep their directly assigned attributes in a dictionary-like namespace. vars lets you inspect it:
vars(tracker) is powerful diagnostic evidence, but normal callers should use the object’s public interface. Reaching into vars to bypass methods would defeat the rules those methods own.
The class has a different namespace:
The method name is defined on the class. distance_km is assigned to this instance by __init__. That distinction prepares us to understand method binding.
Catch misspelled and surprise attributes
When lookup cannot find a requested name, Python raises AttributeError:
Read the instance type and missing name in the message. Here, distnace_km is a typo; installing a package or recreating the notebook will not fix it. Compare the request with vars(tracker) and the class’s documented interface.
Ordinary Python instances also allow a caller to create a new attribute by assignment:
The typo now creates a second field while the real distance remains 0.0. That flexibility is useful in exploratory Python, but it means naming discipline and focused checks matter. Later tools can restrict attributes or catch names statically, but this foundation course first makes the runtime behavior visible.
Class bodies execute when Python reaches the class statement. Avoid placing input, network access, or demonstrations there. This small example prints while the class is being defined, before any instance exists:
Method bodies do not run during class definition; print is directly in the class body, so it does. Real class bodies normally contain method definitions, documented shared settings, and small declarative expressions—not program launch behavior.
4. Watch Python bind a method to one instance
Access the method through the class and through an instance:
The exact addresses vary, but the first representation identifies a function and the second identifies a bound method. The bound method remembers both the underlying function and the instance:
Both results are True.
These two calls have the same effect:
In tracker.record_distance(2.5), Python obtains a bound method and supplies tracker as the first argument. The explicit form calls the underlying function through the class and supplies the instance manually. Prefer the first form in ordinary code; the second form is useful evidence for understanding self.
self is not a keyword. It is the strong Python naming convention for the first parameter of an instance method. Calling it this_object would run, but would surprise every Python reader.
Diagnose a missing self
Consider this definition:
The syntax is valid. The problem appears at the call:
Python binds badge to the first parameter named prefix, then the explicit "Lead" becomes a second positional argument. The function only declares one, so Python reports that two were given.
Repair the signature:
The fix is not “add self because methods always need magic.” It is “declare a parameter to receive the instance Python binds to this method call.”
An instance method lives on the class. Accessing it through an instance creates a bound method for that access.
flowchart LR call[tracker record_distance 2] --> lookup[Find function on class] lookup --> bind[Bind tracker as self] bind --> run[Run function with tracker and 2] run --> state[Update tracker distance]
Checkpoint: trace self and initialization
6. Notice the difference between returning and changing
Methods are still functions. They can return values, cause side effects, or do both. Make that contract deliberate:
class DistanceLog:
def __init__(self):
self._entries = []
def record(self, distance_km):
"""Store a distance and return the new total."""
if distance_km < 0:
raise ValueError("distance_km cannot be negative")
self._entries.append(distance_km)
return sum(self._entries)
def total(self):
"""Return the total without changing the log."""
return sum(self._entries)record changes _entries and reports the new total. total only answers a question. A caller can verify both parts:
Do not return self by habit just to chain calls. Fluent chains can obscure when mutation happens, especially for beginners. Return the result the caller needs, or return None when the action itself is the complete contract.
7. Build a trail counter and explain why it is a class
Create TrailCounter with these names and contracts:
class TrailCounter:
distance_unit = "km"
def __init__(self, trail_name):
"""Start one named trail with zero distance and no checkpoints."""
...
def add_distance(self, amount):
"""Add a non-negative amount and return the new total."""
...
def reach(self, checkpoint):
"""Store a non-blank checkpoint and return its one-based number."""
...
def summary(self):
"""Return `<trail>: <distance> km, <count> checkpoints`."""
...Your implementation must satisfy:
ridge = TrailCounter("Ridge Run")
marsh = TrailCounter("Marsh Walk")
assert ridge.add_distance(2.5) == 2.5
assert ridge.add_distance(1.0) == 3.5
assert ridge.reach("Stone Arch") == 1
assert ridge.reach("North Lookout") == 2
assert ridge.summary() == "Ridge Run: 3.5 km, 2 checkpoints"
assert marsh.summary() == "Marsh Walk: 0.0 km, 0 checkpoints"
assert ridge.checkpoints is not marsh.checkpoints
assert TrailCounter.distance_unit == "km"
try:
ridge.add_distance(-1)
except ValueError as error:
assert str(error) == "amount cannot be negative"
else:
raise AssertionError("negative distance should fail")
try:
ridge.reach(" ")
except ValueError as error:
assert str(error) == "checkpoint cannot be blank"
else:
raise AssertionError("blank checkpoint should fail")After it passes, write four sentences:
- Which attributes belong to each instance?
- Why is
distance_unita class attribute? - What instance becomes
selfinridge.reach("Stone Arch")? - What does the class improve compared with a dictionary and functions?
Hint 1
In __init__, assign trail_name, distance_km, and a newly created checkpoints list to self. Do not place the list in the class body.
Hint 2
Validate before assigning or appending. reach can strip the supplied text, append the cleaned value, and return len(self.checkpoints).
Hint 3
summary only reads state. An f-string can use self.trail_name, self.distance_km, len(self.checkpoints), and self.distance_unit.
Compare a complete implementation after attempting the lab
class TrailCounter:
distance_unit = "km"
def __init__(self, trail_name):
cleaned_name = trail_name.strip()
if not cleaned_name:
raise ValueError("trail_name cannot be blank")
self.trail_name = cleaned_name
self.distance_km = 0.0
self.checkpoints = []
def add_distance(self, amount):
if amount < 0:
raise ValueError("amount cannot be negative")
self.distance_km += amount
return self.distance_km
def reach(self, checkpoint):
cleaned = checkpoint.strip()
if not cleaned:
raise ValueError("checkpoint cannot be blank")
self.checkpoints.append(cleaned)
return len(self.checkpoints)
def summary(self):
return (
f"{self.trail_name}: {self.distance_km} {self.distance_unit}, "
f"{len(self.checkpoints)} checkpoints"
)
ridge = TrailCounter("Ridge Run")
marsh = TrailCounter("Marsh Walk")
assert ridge.add_distance(2.5) == 2.5
assert ridge.add_distance(1.0) == 3.5
assert ridge.reach("Stone Arch") == 1
assert ridge.reach("North Lookout") == 2
assert ridge.summary() == "Ridge Run: 3.5 km, 2 checkpoints"
assert marsh.summary() == "Marsh Walk: 0.0 km, 0 checkpoints"
assert ridge.checkpoints is not marsh.checkpointsThe evolving fields belong to each instance. The immutable unit label is shared intentionally. ridge becomes self for its bound call. The class gives the state one named owner and makes valid operations discoverable at the call site.
Checkpoint: decide whether the class earns its place
8. Key points for your first useful class
- Begin with the simplest working design. Introduce a class when named state ownership and related operations improve the program.
- A class statement creates a class object. Calling the class returns an instance after initialization.
__init__initializes an instance and returnsNone; it is not the method to returnselffrom.- Attributes assigned to
selfbelong directly to that instance. Two class calls can therefore produce independent state. - Functions defined on a class become bound methods when accessed through an instance. The bound instance is supplied as
self. - Class attributes are useful for intentionally shared settings and methods. Mutable per-instance state belongs in initialization.
- An instance attribute can shadow a class attribute.
varshelps you inspect where ordinary state is stored. - A useful class makes responsibilities and valid operations easier to see. It does not earn its place merely by wrapping a dictionary.