Take a Break
5:00
Inhale…
Give your mind a break — no phone, no music, just idle time or a quick walk.
Implementation and Code Sharing — Part 2
Prerequisite Knowledge
This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.
Previously Covered in This Subject
- Robustness and the five features of good code — covered in Lecture 9
- From prototype to production: the ML pipeline — covered in Lecture 2
- Machine learning systems in production — covered in Lecture 1
- Object-oriented programming for ML systems — covered in Lecture 10
- REST, GraphQL, and gRPC communication — covered in Lecture 6
- Separating models from business logic — covered in Lecture 7
11.1 Error Logging and Debugging
11.1.1 The Core Philosophy: Graceful Failure and Transparent Diagnostics
Every system will fail at some point. The lecture opens not with whether your code will break, but with what happens when it does.
The core idea: design systems that fail gracefully and report their diagnostic state transparently. "Graceful" does not mean preventing failure altogether — code will fail, and models will break. It means that when failure happens, the system produces a clear, actionable signal about what went wrong, instead of silently corrupting downstream results.
The professor frames this with a memorable industry quote: "If you don't build logging and error handling into your system, your users will become your loggers." The implication is concrete. In a production environment, if the system itself does not capture what went wrong, end users will file tickets, submit service requests, and flood the support channel with descriptions that may be incomplete or misleading. Building proper logging and error handling is not a luxury — it is the difference between a system that can be diagnosed quickly and one that generates a storm of confused user complaints.
This principle connects directly to the broader theme of the lecture: the gap between code that works on a personal computer in a local sandbox and code that survives the harsh environment of production deployment. A beautifully optimized algorithm in a Jupyter notebook is practically useless if it cannot report what happened when something goes wrong in production.
There is a deeper reason this matters beyond just fixing bugs: an error is when code stops unexpectedly before completing its tasks, and anything that depends on that code may stop too. When a failure is silent, the damage is not confined to one function — it cascades invisibly into everything downstream. Transparent diagnostics stop that cascade at the source.
11.1.2 Robustness in Data Science
What does it mean for a data science system to be robust?
Robustness (in data science): how well the system handles noisy, incorrect, or unexpected input data without behaving unpredictably. When a student was asked to define it, the essence captured it: even when there is noisy data, the program should handle that noise and still predict correctly — or at least fail informatively.
The professor made a striking claim: in data science, input handling accounts for 50 to 60 percent of the work. This is not an exaggeration of a corner case — it is a reflection of reality. The predictive model, the pipeline, and the output all depend on the quality and shape of the data arriving at the system's boundary. Whether you are discussing a project with your manager or answering interview questions in the industry, the question of how you handle data quality will come up repeatedly.
Robustness is not about preventing all crashes. It is about ensuring data pipeline integrity — understanding what the pipeline is processing, what mathematical logic it applies, and what data it includes. The core concept that follows from this is failing early: detect errors as soon as they occur, to prevent silent failures in model results.
Scope: A robust program does not have to succeed on every input. It has to behave predictably on every input. Failing loudly with a clear message is predictable behavior; returning a wrong prediction silently is not.
Real-world: In industry, data quality issues are the single largest source of ML system failures. The concept of failing early maps directly to data validation stages in ETL (Extract, Transform, Load) pipelines used in data warehousing, where raw data is cleaned and transformed into usable information before it enters any downstream system.
11.1.3 The Concept of Failing Early (Failing Fast)
The concept of failing early — sometimes called failing fast — is one of the most practically important ideas in the lecture. The professor explains it from the very basic level to the design level.
At its most basic, failing early means: when your code receives incorrect input, raise an error immediately rather than allowing that corrupted data to propagate further down the pipeline. If a model expects a feature vector of length 3 and receives one of length 2, do not proceed with a default or silent correction — raise an exception that says exactly what happened.
Intuition — the airport security analogy: think of a data pipeline as passengers boarding a plane. Raw data is the crowd at the entrance — some travelers carry liquids, oversized bags, or wrong documents. Security screening (validation) happens before boarding, not after takeoff. If bad data passes screening and reaches the model, the failure is discovered miles away from where the problem started, and the cost of fixing it is far higher. Screening early filters out a percentage of bad data immediately so that what the model sees is clean.
The professor draws a parallel to data warehousing: in any ETL pipeline, the first stage is always cleaning raw data. Raw data contains noise — numbers where strings are expected, special characters in numeric fields, missing values, mismatched types. The concept is the same whether you are building a data model, writing exception classes in Java, or designing filters in Python: pass the data through validation at the earliest possible stage so that bad data is caught before it can corrupt downstream processing.
A student asked a practical question about how to ensure failing early in real-world scenarios. The professor's answer was layered:
Q: I understand the concept of failing early. It makes a lot of sense, but practically, how do we ensure that?
A: Failing early can come from many angles. The first thing is how well you handle your data. You get raw data that may contain noise — someone put a number where a string is expected, special characters in numeric fields. Whatever the raw data you have, all kinds of noise tend to make your code fail. It is our job to make sure that failure happens at the very first stage. You can put a filter — conceptually, not just in code terms. It can be a data model, a class exception, an OOP concept — anything. Whatever you pass through, make sure you pass it through at an early stage so that a certain percentage of bad data gets filtered out immediately. When we run models, we always make sure that clean data gets passed first, not noisy or raw data. This is the concept of robustness — it is not just about crashing, it is about catching the crash early.
The student followed up, asking whether scenarios beyond data type mismatch need to be anticipated. The professor's answer broadened the scope significantly: there are many scenarios, and the exceptions shown in the lecture (value errors, key errors, type errors) are just the Python-specific ones. The conceptual design principle is language-agnostic. When you move from a coding world to a designing world — which every software developer must eventually do — you stop thinking in terms of "I'll put a class here" and start thinking about what the business needs at the architectural level.
The professor illustrated this with a business scenario: a customer gives you a CSV file with 5,000 rows, but they only need data for five specific business profiles. The answer you give the business is not "I'll write an exception class" — it is "we will filter out and work only on the five profiles that matter to your business." The implementation behind that can be OOP classes, data models, SQL filters, or anything else. The key insight: you discuss with stakeholders from a design perspective, not a code perspective.
Real-world: This distinction between design-level thinking and code-level thinking is one of the most important career transitions in software engineering. Junior developers think in code; senior engineers and consultants think in systems and architecture.
11.1.4 Common Python Exceptions for Data Science
The lecture identifies several common Python exceptions that data science practitioners encounter regularly. These are not coding trivia — understanding what each exception means saves significant debugging time.
| Exception | When it is raised | Data science example |
|---|---|---|
ValueError |
A function receives an argument of the right type but an inappropriate value | Passing a feature vector of the wrong dimension to a model that expects a specific shape |
KeyError |
A dictionary-like lookup fails because the key does not exist | Accessing a missing column name in a Pandas DataFrame, often due to typos or schema changes |
TypeError |
An operation receives an argument of the wrong type | Passing a string to a function that expects a numeric value; NumPy array operations receiving non-numeric data |
IndexError |
A sequence index is out of range | A loop that iterates beyond the bounds of a list during iterative processing |
The professor noted that these are assessment-relevant: students should know the definitions and descriptions of these errors, as they may appear in exams. There are many more exceptions in Python, but these four are the most common in daily data science work.
Exam note: understanding the definitions and descriptions of common Python exceptions (ValueError, KeyError, TypeError, IndexError) is important for assessments. Be able to match each exception to a realistic data science scenario.
11.1.5 Handling Errors Professionally
The lecture's treatment of professional error handling centers on a simple but powerful practice: use try-except blocks to catch specific exceptions, not bare except clauses.
A bare except catches everything, including system interrupts and memory exhaustion errors. When that happens, the system enters what the professor calls a "zombie state" — it is technically running, but you have no idea what went wrong, what to search for, or what to fix. The error information is lost in a catch-all net.
The zombie state trap: a bare except that silently swallows errors is worse than crashing. The code keeps running, but every result from that point forward is suspect, and there is no trace of what went wrong. A system that crashes loudly tells you something; a zombie tells you nothing.
The professional pattern is:
- Wrap risky operations in a
tryblock. - Catch specific exceptions (e.g.,
ValueError,KeyError) in separateexceptblocks. - Write a descriptive log message using the logging library when an exception is caught.
- Return clean, predictable default values rather than crashing or silently ignoring the error.
import logging
def predict_with_fallback(features):
try:
result = model.predict(features)
return result
except ValueError as e:
logging.warning("Feature vector malformed: %s. Returning default.", e)
return DEFAULT_PREDICTION
except KeyError as e:
logging.error("Missing expected column: %s", e)
raise
This is what the professor calls customizing domain-specific logic: when an exception is caught, do not just ignore it. Log it with enough context that someone reading the log later can understand what happened, what data triggered it, and what the system did in response. The goal: even a person who does not understand the entire codebase should be able to look at the error log and know what predictable values or fallback behavior the system applied.
The same principle extends the try/except pattern: an else block can run when no error is raised (for the success path), and a finally block runs whether or not an error occurred — most often used to release resources such as closing an open file. Good error messages matter as much as good code: an informative message should describe what the problem was and what to do to fix it.
11.1.6 Interpreting Python Tracebacks
The debugging process described in the lecture follows a generic, domain-independent cycle:
- Reproduce the bug — Replicate the issue in the lower (development/staging) environment.
- Locate the bug — Identify where in the code the failure occurs.
- Identify the root cause — Understand why the failure happens, using test data and performance data.
- Fix the bug — Implement the correction.
- Test the fix — Thoroughly verify the fix before pushing to production. Testing can be single-stage or multi-stage depending on the project and client agreements.
- Document the process — Write down what happened, why, and how it was fixed. This is critical because new team members need to understand past fixes, and some fixes require process-specific or infrastructure-related knowledge that is not obvious from the code alone.
The professor connects this to a specific Python skill: interpreting tracebacks. The key rule is to read tracebacks from bottom to top. The last line tells you the exact error type and message. Then you trace upward through each function call that led to the error, noting file names and line numbers at each step. Each step in the traceback shows the chain of function calls, providing the information needed for rapid diagnosis.
The professor's phrasing: "We call it a traceback because we have to go in a backward direction." Do not jump to the top of the traceback and look at the first print statement — start at the bottom where the error actually occurred, then work your way up through the call chain.
Reading a traceback, step by step. Suppose a prediction script fails with this traceback:
--------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[7], line 1
----> 1 fit_trendline(timestamps)
Cell In[6], line 2, in fit_trendline(year_timestamps)
----> 2 result = linregress(year_timestamps, data)
3 slope = round(result.slope, 3)
4 r_squared = round(result.rvalue**2, 3)
NameError: name 'data' is not defined
- Start at the bottom:
NameError: name 'data' is not defined— the error type and message. - Look at the last code frame above it: the arrow points to line 2 of
fit_trendline, wherelinregressis called. This is where the error surfaced. - Check the frame above that: the caller
fit_trendline(timestamps)— only one argument was supplied. - Trace the cause: the arrow points at line 2, but the root cause is in the function definition — the
dataargument was never declared, so the namedatadoes not exist when the function runs.
The error appeared in the call to linregress, but the bug lives in the function signature. This is why you read the whole chain, not just the last line.
Visual intuition: picture the traceback as a stack of boxes, one per function call. The topmost box is the first function entered; the bottommost is the frame where the error actually occurred. The error message printed at the very bottom is the "crime scene" — the arrows and line numbers above it are the "trail of evidence" leading back to the root cause. Most beginners read top-down and get lost in entry-point noise; experts read bottom-up and reach the cause in seconds.
11.1.7 Custom Exceptions for Domain-Specific Failures
As data pipelines grow in complexity, native Python exceptions like ValueError and TypeError become too generic. They tell you something went wrong, but not what went wrong in domain terms.
The professor's advice: extend the base exceptions into custom, domain-specific ones. For example, instead of raising a generic ValueError when a model receives a feature vector of the wrong size, raise an IncompatibleFeatureSizeError that explicitly states: "Model expects 3 features but got 2." Instead of a generic error when data contains missing values, raise a MissingDataError that says: "Input data contains NaN values. Cannot compute prediction."
Defining and using custom exceptions. A custom exception is just a class that inherits from the base Exception class:
class IncompatibleFeatureSizeError(Exception):
"""Raised when a feature vector does not match the model's expected shape."""
class MissingDataError(Exception):
"""Raised when input data contains missing (NaN) values."""
def validate_input(features, expected_size):
if len(features) != expected_size:
raise IncompatibleFeatureSizeError(
f"Model expects {expected_size} features but got {len(features)}."
)
if any(value != value for value in features): # NaN never equals itself
raise MissingDataError(
"Input data contains NaN values. Cannot compute prediction."
)
The error message itself is now a diagnosis: the problem, the expectation, and the observed reality are all in one line. Debugging becomes faster because the message tells you exactly what domain-level problem occurred — you save time, get a much more refined perspective on the failure, and can trace specific domain failures in ML workflows efficiently.
A secondary benefit the lecture implies: custom exception names act as a vocabulary for the codebase. When an IncompatibleFeatureSizeError appears in a log file, the whole team immediately knows which part of the system produced it — errors from different parts of the codebase no longer get mixed up.
11.1.8 Logging: Why It Beats Print
The lecture draws a sharp distinction between print statements and proper logging. For small, quick debugging during development — a few lines of code, a small model — print is fine. You run the code, you see the output, you close the session.
But as soon as the system grows, logging wins decisively:
| Capability | print |
Logging |
|---|---|---|
| Persistent history | Output disappears the moment the terminal session closes | Logs are stored in files, databases, or observability platforms |
| Categorization | One stream, no levels | Severity levels (debug, info, warning, error, fatal) let you filter messages |
| Metadata | Only what you choose to print | Timestamps, module names, and line numbers captured automatically |
| Production usability | No access to console output on deployed systems | Remotely searchable and queryable |
The 48-hour training job. Imagine launching an ML training job on an AWS EC2 instance that runs for 48 hours. You use print statements to track epochs, losses, and accuracy. Your SSH session disconnects — or the terminal buffer clears — and that information is gone forever. A properly configured logging system pipes all statements to a persistent file or observability platform like CloudWatch or Elasticsearch. The data survives session disconnects, terminal clears, and infrastructure hiccups. Anyone with access — team lead, project manager, business owner — can query those logs to understand why the model is not behaving as expected, what parameters it was using, and what errors occurred.
The lecture's practical rule of thumb: print for quick throwaway checks in a notebook or a 10-line script; logging the moment the code will outlive the current session. There is no size threshold that is exactly right for every team, but if the output matters after the process ends, it belongs in a log. Error messages benefit even more: the traceback itself can be recorded in the log, preserving the full call chain for later diagnosis.
Real-world: CloudWatch (AWS) and Elasticsearch are live examples of observability platforms where logs persist and can be searched. In production systems, log viewers and observability tooling pick up output from the Python logging module directly, which is why configuring it properly matters — the log is the system's memory.
11.1.9 Standard Logging Levels
The lecture identifies the standard logging levels used in production systems:
| Level | Meaning | Typical data science example |
|---|---|---|
| DEBUG | Detailed information for troubleshooting | Variable states, intermediate computation results, entry and exit points of functions |
| INFO | Operational messages about system status | "Model loaded successfully," "prediction generated," "data pipeline started" — the routine heartbeat |
| WARNING | Unexpected but not fatal | Missing features, falling back to default values, deprecated API usage — investigate before it becomes a real problem |
| ERROR | A serious problem: an operation failed | A specific prediction could not be computed; the system keeps running, but this operation failed — where most production monitoring focuses |
| FATAL | The entire system crashes; no recovery from within the app | Memory exhaustion — e.g., expecting a model to run on 60 GB of memory when only 2 GB is available; the system simply cannot continue |
The professor's practical guidance: in production environments, the typical choice is the ERROR level — you want to see what errors are occurring. In non-production environments, you can use WARNING (for preemptive alerts) or INFO (to see every line of code producing a log entry), depending on how much detail you need. The default level in Python's logging module is WARNING, so INFO messages are silently dropped unless you explicitly lower the threshold with logging.basicConfig(level=logging.DEBUG).
Scope: the levels are a filter, not a diary requirement. Setting the level to DEBUG in production floods the log storage with noise and buries the errors you actually care about; setting it to ERROR in development hides warnings that would have caught problems early. Match the level to the environment, and match the environment to the stage of the pipeline.
Exam note: understanding the difference between logging levels and when to use each one in production vs. non-production environments is important. Know all five levels, what each captures, and which one production monitoring typically uses (ERROR).
11.1.10 Worked Example: Robust Error Handling and Logging Demonstration
The professor demonstrated a Python file that implements robust error handling and logging. The code defines:
- Professional logging configuration — Uses the
loggingmodule instead ofprint. - Custom domain-specific exceptions — Two classes:
IncompatibleFeatureSizeError— Raised when the input feature vector does not match the model's expected dimension.MissingDataError— Raised when the input contains NaN (Not a Number) values.
- A mock robust model — Simulates a prediction pipeline that expects a specific feature shape.
The demonstration, reconstructed. The structure of the demo code is:
import logging
import numpy as np
logging.basicConfig(
filename="model_service.log",
level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(module)s: %(message)s",
)
class IncompatibleFeatureSizeError(Exception): ...
class MissingDataError(Exception): ...
class RobustModel:
def __init__(self):
self.expected_features = 3
def validate(self, features):
if len(features) != self.expected_features:
raise IncompatibleFeatureSizeError(
f"Model expects {self.expected_features} features "
f"but got {len(features)}."
)
if np.any(np.isnan(features)):
raise MissingDataError(
"Input data contains NaN values. Cannot compute prediction."
)
def predict(self, features):
try:
self.validate(features)
except IncompatibleFeatureSizeError as e:
logging.warning("Validation failed: %s. Falling back to default prediction.", e)
return DEFAULT_PREDICTION
except MissingDataError as e:
logging.error("Data corruption detected: %s. Halting pipeline for this record.", e)
return None
logging.info("Prediction generated successfully.")
return MODEL_WEIGHTS @ features
The demonstration runs three scenarios:
Scenario 1: Clean data. Input array [2.5, 3.1, 4.0] — the model expects 3 features and receives 3. Validation passes, and the prediction is computed. Output: an INFO level log — "Prediction generated successfully." No errors, no warnings.
Scenario 2: Dimension mismatch. Input array [2.5, 3.1] — the model expects 3 features but receives only 2 (missing the 4.0 value). Output: a WARNING level log — "Validation failed: model expects 3 features but got 2. Falling back to default prediction." This is a warning, not an error, because the system can still respond gracefully by falling back to a default prediction.
Scenario 3: Corrupted data. Input array [2.5, np.nan, 4.0] — the model expects numeric values but receives NaN in one position. Output: an ERROR level log — "Data corruption detected: input contains NaN values. Cannot compute prediction. Halting pipeline for this record." This is an error because no meaningful prediction can be produced from corrupted data, and the pipeline halts for that specific record rather than propagating bad data downstream.
Why three levels, not one? The severity of the log message mirrors the severity of the failure's impact: a fallback can still serve a user (warning), corrupted data cannot (error), and a crashed process changes nothing for the worse (fatal). Choosing the right level tells future readers — and monitoring systems — how urgent the message is without reading a word of the code.
The professor emphasized that this demonstration shows graceful degradation: the system does not crash entirely when it encounters bad data. Instead, it reports what happened at the appropriate severity level, takes the best action it can (fall back to default or halt the record), and continues operating for clean data.
The code also demonstrated the use of VS Code's built-in debugger — the "click to add a breakpoint" feature that stops execution at a specific line and shows all variable states up to that point. The professor noted that the Python debugger (pdb) is also an important tool for stepping through code.
Exam note: students should go through this demonstrated code after class and try to integrate the patterns into their own code to understand error handling and logging in practice. The three-scenario mapping — clean data → INFO, recoverable problem → WARNING, unrecoverable corruption → ERROR — is the takeaway pattern.
11.1.11 Debugging Strategies
The lecture presents three systematic debugging strategies:
Strategy 1 — Rubber duck debugging. Explain the code line by line, out loud, to find logic errors. This traditional technique works because the act of articulating what each line does forces you to confront assumptions and spot flaws you glossed over while reading silently.
Strategy 2 — Divide and conquer. Isolate code blocks to find the smallest failing example. Split the code into small blocks based on the functions they perform; test each block independently. When a block passes, move to the next; when one fails, you have isolated the problem. This is especially useful when you are not using a debugger.
Strategy 3 — Using debuggers. Step through code using pdb (Python Debugger) or IDE-integrated tools like VS Code's debugger. Set a breakpoint — a stop point — where execution pauses. The debugger shows all variable states and execution flow up to that point, giving complete visibility into what the code was doing before the breakpoint.
The professor connected the divide-and-conquer strategy to logging at the module level: if you have divided your code into small, testable blocks, you can place logging at each block boundary. This is closely related to the modularity concept discussed later in the lecture (Section 11.4).
Pitfalls of debugging:
- Fixing without reproducing — you cannot confirm a fix if you cannot reproduce the bug; the reproduce step in the debugging cycle exists for a reason.
- Changing several things at once — if three changes go in and the bug disappears, you do not know which one mattered; change one thing at a time and observe.
- Trusting the traceback's first line — the error surfaces where the bad value is used; the root cause is often in an earlier frame, as the
dataargument example in 11.1.6 showed. - Leaving breakpoints behind — a
breakpoint()call left in production code will pause execution at runtime; remove debugger hooks after the bug is fixed.
Recap + bridge. The philosophy of this section in one line: fail early, fail loudly, log everything, and debug systematically. These habits — validating at the boundary, raising informative exceptions, and logging at every meaningful stage — are the foundation for everything else in this lecture. The next topic, code formatting and linting, attacks a different kind of error: the ones that never produce a traceback at all, because the code "works" but is unreadable and error-prone by design.
11.2 Code Formatting and Linting
11.2.1 PEP 8 — The Python Style Guide
Code formatting and linting represent the shift from subjective styling arguments to programmatically enforced code validation. The professor's framing: it does not matter if you write 200 lines or 150 lines, or if you use single quotes or double quotes — what matters is that the style is consistent, because consistency makes code readable, and readability makes bugs easier to spot.
Why style matters — the "read once, read often" fact. The professor quoted a key principle: "Code is read much more often than it is written." Only a few people write any given piece of code, but many people will read it — teammates, reviewers, future maintainers, and even yourself six months later. If you are conceptually aware of what the code does, readability ensures you can navigate it quickly.
The foundation of Python style is PEP 8 (Python Enhancement Proposal 8) — the official style guide for Python, written in 2001 by Guido van Rossum (Python's creator), Barry Warsaw, and Nick Coghlan, and adopted as the community default. PEP 8's own motto captures the hierarchy: consistency with the style guide is important, consistency within a project is more important, and consistency within one module or function is the most important.
PEP 8 covers:
- Naming conventions — Variables and functions must use
snake_case(lowercase with underscores), while classes must usePascalCase(capitalized words without underscores). - Whitespace usage — Always use 4 spaces per indentation level, never tabs. Place single spaces around mathematical operators.
- Import ordering — Standard library modules first, followed by third-party packages, then local project imports. Each group is separated by blank lines.
Import ordering in practice. PEP 8 requires imports in three groups, standard → third-party → local:
# standard library
import os
import sys
# third-party
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
# local application imports
from project.data_loader import load_data
Tools like isort sort imports automatically — you never need to do this by hand. The reason for the discipline: a clear, ordered import list makes dependencies visible, and forgotten imports are one of the most common causes of runtime errors.
The professor's practical advice: do not argue about styles. Let the tool decide. Whatever recommendation the tool gives, follow it, and you will be fine. PEP 8 reduces cognitive load for teammates and eliminates style-based debates. Consistent formatting even reduces syntax errors, because with standardized code it is easier to know what to expect — mismatched parentheses and forgotten colons are caught by pattern, not by luck.
11.2.2 Black - The Uncompromising Formatting Tool
Black is a code-formatting tool that automates PEP 8 compliance with zero configuration. Its value proposition:
- Saves time on manual alignment decisions — you write "ugly" code quickly, save the file, and Black restyles it consistently.
- Eliminates style-based merge conflicts — when two developers format the same code differently, Git cannot reconcile the changes; a single canonical format removes the conflict at its source.
- Focuses attention on logic over layout - the tool handles spacing, line length, and bracket placement so developers can focus on what the code does.
What Black actually changes. Given a line like:
ax.plot(n, big_o[ i], label= line_names[i ])
Black rewrites it to:
ax.plot(n, big_o[i], label=line_names[i])
It removes spaces inside brackets, normalizes quotes, enforces consistent blank lines around definitions, and reformats lines to a sensible length. The name comes from Henry Ford's famous offer: "any color, as long as it's black" — Black's style is equally uncompromising. You can preview changes with black my_script.py --diff and exclude specific lines with # fmt: skip.
The professor's emphasis: Black ensures that the logic is presented in a well-defined, standard way, not that the layout is aesthetically pleasing. The layout is secondary; the logic is primary. Black formats appearance only — it never changes what the code does.
11.2.3 Ruff — The Modern Python Linter
A linter scans code for potential errors through static analysis — analyzing the code without actually running it. In traditional software engineering terms, this is somewhat analogous to compilation: checking syntax, variable names, unused imports, unreachable code, and improper spacing before execution. The name is a joke from 1978: the original lint tool for C "removed fuzz" (bugs) from code, like the lint trap in a clothes dryer.
Why linting is a time-saver. A syntax error crashes your code immediately, but a misspelled variable name only fails on the line that uses it — which, in a long-running training script, could be hours in. Linters find these errors before you run the code. The category breakdown matters: messages starting with E are genuine errors (e.g., E0602 undefined name), while messages starting with C are style/convention issues.
Ruff is the modern, fast, all-in-one linter for Python. The professor noted that earlier tools like Flake8 and Pylint were used, but Ruff has become the standard in current practice. When you run Ruff:
- It scans the code statically (without running it).
- It checks for variables, parameters, unreachable code, improper indentation, and spacing.
- It provides feedback on all issues found.
Ruff also separates linting from formatting: ruff check my_script.py reports issues, ruff check --fix auto-fixes what it safely can, and ruff format applies formatting like Black.
A student asked a practical question about Ruff's behavior with Git:
Q: Does Ruff make changes to the code if we combine Ruff with Git operations?
A: Yes, it does. When you run Ruff, it makes sure that arguments are passed with the right spaces, that functions are well-defined. This is also a part of your code — your function signatures are part of your code. So yes, Ruff can make actual changes to your code. You can run it locally with ruff check --fix to auto-fix issues. Generally, this is caught in a CI/CD pipeline — if the code is not properly linted, the pipeline fails at that step, and you have to fix and recommit.
A student with a Java background shared their experience with similar linters in Java ecosystems: after writing code, a single command automatically checks and fixes formatting issues. If developers forget to run the linter locally, the CI/CD pipeline catches it and blocks the commit. This is the standard workflow across languages.
Another student described a more advanced setup: spec-level development, where coding standards and formatting rules are defined in a rule book. The build tool automatically restructures code according to these rules — for example, if a developer does not follow the naming convention (capital vs. camel case), the rule engine automatically corrects it and enforces compliance before allowing a commit.
Pitfall — inconsistent linting. Because each linter uses a slightly different style guide, results differ: Pylint may flag a missing docstring, Flake8 a missing newline at end of file, Ruff an undefined name. Choose one linter and agree on it as a team. Mixing tools or ignoring the agreed standard recreates exactly the style debates that linting was meant to eliminate.
Real-world: CI/CD pipelines typically include a linting step that blocks commits or merges if code does not pass the linter. This is a standard industry practice across Python, Java, and other languages — the professor's student shared the Java equivalent, and the lecture notes the same enforcement in Python shops.
11.2.4 Type Hints and Mypy
Type hints in Python allow static typing benefits: self-documenting code, IDE auto-completion, and error highlighting. When you annotate a function with type hints — specifying that parameters are float and the return type is list[float] — you create a contract that tells both developers and the machine exactly what types are expected.
The contract pattern. Type annotations follow the form my_variable: type:
def fit_trendline(year_timestamps: list[int], data: list[float]) -> tuple[float, float]:
...
The annotation states the expected input types and the return type. It does not change how the code runs — Python ignores them at runtime — but it makes the function's contract visible to every reader and every tool.
Python is a dynamically typed language: a variable can change type mid-run (x = 10 then x = "hello" is legal). That flexibility is a constant source of bugs — a function receives a str where a numeric was expected and fails with TypeError: must be real number, not str. Mypy is a static type checker that parses these annotations and checks for type mismatches across the entire codebase without running the code. If a function is annotated to accept float but receives a str at some call site, Mypy flags it. This is a form of static linting specific to the Python type system.
Mypy catching a wrong annotation. Suppose mode_using_counter is annotated as accepting a float but its body expects a list:
from collections import Counter
def mode_using_counter(list_of_numbers: float) -> float:
c = Counter(list_of_numbers)
return c.most_common(1)[0][0]
The code runs fine — Python never checks annotations. But mypy mode_using_counter.py reports:
error: No overload variant of "Counter" matches argument type "float" [call-overload]
The annotation was wrong: it should be list[float]. Mypy caught the inconsistency before anyone called the function with the wrong type in production. This is the same protection the other tools provide, but for the type system specifically.
Scope: type hints are optional in Python by design (PEP 484) and remain controversial — some teams love them, some find them noisy. The rule: follow your team's convention. Type checking only helps if a tool actually checks the annotations — without mypy (or an IDE extension like Pyright), an annotation is just a comment with a colon.
Recap + bridge. Formatting and linting move style from opinion to automation: PEP 8 defines the standard, Black formats it, Ruff checks it, and Mypy extends the check to types. All four tools share one philosophy from the professor: let the tool decide, and enforce it in CI. This is the first real difference between research-style code and the disciplined code the next topic demands — research versus production code.
11.3 Research Versus Production Code
11.3.1 Defining the Two Worlds
The lecture draws a clear conceptual line between research code and production code. These are not just different environments — they represent fundamentally different mindsets, priorities, and engineering practices.
The framing: a Jupyter notebook is where you ask "does this work?" A production system is where you answer "does this keep working, for everyone, all the time?" These are different jobs, and the code for each looks different on purpose.
| Dimension | Research Code | Production Code |
|---|---|---|
| Goal | Discovery, proof of concepts (POCs) | Repeatable service, robustness |
| Environment | Jupyter notebooks | Python scripts, Docker containers |
| Priority | Speed of insights | Stability and scalability |
| State | Manual, interactive execution | Automated CI/CD execution |
The professor emphasized that in the production cycle, all four dimensions matter, but stability and scalability carry the most weight. The reasoning: you can push 10 production codes daily, but if those codes are not stable — if they cannot scale to handle different data inputs and varying loads — there is no point deploying them quickly. Stability and scalability are the non-negotiable requirements of production.
Exam note: if asked in an interview "What are the most important things when deploying code to production?", the answer is stability and scalability.
11.3.2 The Research Mindset
When exploring a brand-new dataset where you do not know which features will be predictive or which model architecture will fit, the research mindset is appropriate. The goal is rapid discovery and validation: "Does this work? If I scale it up, how does it behave?"
The freedom of the research environment comes with significant drawbacks: global variables scattered everywhere, duplicated code, manual execution order. These are acceptable during experimentation because the goal is learning, not reliability. The professor's summary: research is where you do all kinds of experimentation, discovery, speed testing, and manual execution. The moment you decide what to build and deploy, everything changes.
Intuition — the kitchen analogy: research code is cooking at home — tasting, improvising, leaving ingredients on the counter. Production code is a restaurant kitchen — every dish follows the same recipe, every station has a defined role, and the health inspector (tests) visits regularly. You cannot run a restaurant the way you cook at home, and you would not want to cook for yourself with restaurant bureaucracy. The wrong mindset for the wrong context is the root of most ML project failures.
11.3.3 The Hidden Problem: Transitioning from Research to Production
The transition from research to production is where many ML projects stumble. The professor identifies several hidden problems:
- Notebooks are fragile — Out-of-order execution can happen (running cell 5 before cell 3), creating inconsistent state that is invisible in the notebook but breaks in production.
- Hidden state — Global variables set during interactive exploration may not be present when the code runs as a standalone script. The globals are part of the notebook's implicit state, not explicitly declared in the code.
- Version control challenges — Notebooks are difficult to version control meaningfully because their JSON structure mixes code, output, and metadata.
The silent killer — hidden state. In a notebook, model = RandomForestClassifier() lives in cell 2, and model.fit(X, y) in cell 8. You ran cell 8 after tweaking something in cell 6 — everything worked. Then the script version runs top to bottom and fails, because cell 8 depended on a variable defined in a cell that never runs in sequence. The notebook never complained; production has no tolerance for it. This is why verified logic must move into .py modules, where execution order is explicit.
The professor's advice: move verified logic from notebooks into .py module files as soon as the experiment succeeds. This is the bridge between research and production — extracting the working code from the interactive environment into structured, importable modules.
This is also the motivation for Docker: when pushing code from a lower environment to different environments, you must encapsulate everything in a Docker container. This ensures that the hidden state problems do not appear when the code runs in a different environment. The container carries the environment with it, so "it worked on my machine" stops being an acceptable excuse.
11.3.4 Feature-by-Feature Comparison
The lecture provides a detailed comparison of research and production code across several dimensions:
| Feature | Research Code | Production Code |
|---|---|---|
| Unit of work | Notebook cells | Modular functions, classes |
| Error handling | Manual inspection | Logging and exceptions |
| Testing | Visual plotting | Unit and integration testing |
| Configuration | Hard-coded parameters | Environment config files (settings files, YAML files in DevOps) |
| State | Interactive, global | Local scope, encapsulated in containers |
The "state" dimension, unpacked. In research, you can test any number of global parameters — X, Y, Z, A, B, C — with no limits; the workspace is your sandbox. In production, the system is encapsulated: it operates within defined limits, processing only the data that conforms to its expected parameters. Production code is scoped to what the business needs, not to everything the researcher explored. Encapsulation is what makes the system predictable — the price of predictability is that you can no longer reach in and change things mid-flight.
The deeper reason for the split is captured by the whole comparison: each column is a different contract. Research code contracts with the researcher (fast feedback, flexible), production code contracts with the business (predictable, scalable, auditable). Trying to satisfy both contracts with one codebase fails both.
Recap + bridge. Research code optimizes for discovery; production code optimizes for stability and scalability. The bridge between them is discipline: modular files instead of notebook cells, logging instead of prints, tests instead of visual inspection, config files instead of hard-coded values, and encapsulated state instead of globals. The next topic, designing and refactoring, is the engineering practice that turns research-grade code into production-grade code without breaking what already works.
11.4 Designing and Refactoring
11.4.1 What Is Refactoring?
Refactoring is changing a software system's internal structure without modifying its external behavior. The professor's example: if a module is expected to produce a certain output in 2 seconds with specific parameters (e.g., ages between 15 and 30), that is the external behavior. Inside that module, you can modify anything — rewrite algorithms, restructure classes, optimize data flow — as long as the output arrives on time with the same parameters.
The refactoring contract. External behavior means: same inputs in, same outputs out, same timing, same format. Good designers can refactor 1,000 lines of code into 100 lines without changing what the system does from the outside. Whatever is expected should come out in the same time, with the same parameters, in the same format.
Refactoring is a normal part of any code project, not a sign of failure — requirements change, code becomes hard to read, and structure needs improvement. The discipline is: make a small change, run your tests to confirm nothing broke, then save (commit). Incremental changes, not a from-scratch rewrite; never throw away parts that work fine. Tests are the safety net that makes refactoring possible — without them, you cannot tell whether the behavior stayed identical.
11.4.2 SOLID Principles for ML
The SOLID principles form the architectural foundation for refactoring. The professor covers three of the five in detail:
S — Single Responsibility: One function, one task. If ten people work on a module, all ten may write functions, but each function should do exactly one thing. When someone looks at a function, it should be immediately clear what it does. If the same behavior is implemented in two different ways by two different developers, that is a violation of single responsibility. The professor's phrasing: "One function means when somebody looks into that function, it cannot give them a reflection that this was developed by somebody else."
O — Open/Closed Principle: Classes should be open for extensions but closed for modifications. You can extend classes (write subclasses, add libraries) but you should not modify existing classes, because modifying a class changes its external behavior. The moment you modify a class, the external behavior gets changed — which violates the refactoring contract. Instead, extend the class to achieve the desired behavior.
D — Dependency Inversion: Depend on abstractions (interfaces), not concrete implementations. If module A depends on module B, and module B depends on module C, those dependencies should be explicitly defined through interfaces, not through hard-coded concrete references. The professor's warning: "If you forget the B dependency, the code will show perspective errors." The goal is to avoid situations where a change in one module cascades unpredictably through the system because of hidden, hard-coded dependencies.
The three principles in one picture. Single Responsibility decides how many things a piece of code should do (one). Open/Closed decides how you change a piece of code (extend, don't rewrite). Dependency Inversion decides what a piece of code may depend on (abstractions, not concrete references). Together they keep the system flexible at every level.
Exam note: the SOLID principles are explicitly tied to the textbook and are assessment-relevant. Know all three covered here — their names, their meaning, and the professor's phrasing for Single Responsibility.
11.4.3 Modularity in Practice
Modularity means breaking a monolithic train-and-predict script into components: data loader, pre-processor, model trainer, evaluator. The benefits:
- Reusability — Once a component is tested and verified, it can be reused across different projects. A well-tested data loader does not need to be rewritten for each new project.
- Testability — Individual components can be tested in isolation. If the data loader works and the pre-processor works, you can narrow down bugs to the model trainer or evaluator.
Intuition — the LEGO analogy: a monolithic script is a single plastic block molded into one shape — change any part and you have to remold the whole thing. Modular code is a box of LEGO bricks: each brick has one simple purpose, bricks are reused across different builds, and a broken brick is swapped out without dismantling the castle. The same relationship holds in code: small, single-purpose pieces compose into larger systems (composability) and are easy to test one at a time.
The professor's practical advice: modularize once your script grows past a few hundred lines. Extract helper functions into separate files — utils.py for shared helper functions, preprocessing.py for feature engineering. A good rule for dividing code: if describing what a function does requires the word "and" — "this function cleans the data and creates the visualization" — it should be two functions.
11.4.4 Dependency Inversion in ML
The professor makes a specific point about hard coding in ML systems: avoid hard-coding specific data source paths or model types. Whenever you hard-code a path like /data/training_set.csv or a model type like RandomForestClassifier, you create a tight coupling that makes the system brittle. When the data source changes or you want to try a different model, you have to hunt through the code and change every reference.
The dependency inversion principle in ML says: depend on abstractions. Use configuration files, environment variables, or dependency injection to specify data sources and model types, so that changing them requires modifying one configuration point, not scattered code references.
Hard-coded vs. configurable.
# brittle: the path and model are welded into the code
df = pd.read_csv("/data/training_set.csv")
model = RandomForestClassifier(n_estimators=100)
# flexible: one configuration point decides the data source and model
DATA_PATH = config["data_path"] # from settings file or environment variable
model = load_model_from_config(config) # model type comes from the config too
When the data moves or the team wants to try gradient boosting, the second version changes one config file; the first version changes every reference scattered through the codebase.
Scope: configuration-driven design has a boundary. It is for things that change between environments and experiments — paths, model types, hyperparameters, connection strings. It is not a license to make every constant configurable; a config file with two hundred one-time-only entries is its own code smell.
11.4.5 Code Smells
Code smells are indicators that refactoring is needed. The lecture identifies four key code smells:
- Long functions — If a function does more than one thing, split it up. A function that loads data, preprocesses it, trains a model, and evaluates results should be four separate functions.
- Duplicate logic — If you copy-paste cleaning code across multiple files, extract it into a shared utility. Duplicate logic means that when you fix a bug in one copy, you have to remember to fix it in all the others.
- Large classes (God objects) — Classes that manage data, models, and the UI simultaneously are too large. Break them into focused classes with single responsibilities.
- Hard-coded paths — Do not hard-code file paths, database connections, or model parameters. Use environment-specific configuration files and variables.
Pitfall — the duplicated-bug tax. Duplicated logic is deceptively dangerous: every copy of the code is a place a fix must be repeated. Fix the cleaning bug in utils_v2.py but forget legacy_pipeline.py, and the same bug resurfaces months later with a different error message. Extract once, fix once.
The professor emphasized that the first and fourth code smells — long functions and hard-coded paths — are the most important to address.
11.4.6 Actionable Refactoring Techniques
The lecture covers three concrete refactoring techniques:
- Extract functions — If you have a block of 15 lines within a main script that handles a specific task, extract it into a named function. This makes the main script shorter and the extracted function reusable and testable.
- Rename variables — Move from generic single-letter variables like
x,df, ormto explicit names likeuser_demographic_dforrandom_forest_model. Explicit names make code self-documenting. - Modularize — Once a script grows past a few hundred lines, extract helper functions into separate files:
utils.pyfor shared utilities,preprocessing.pyfor feature engineering functions.
Why rename is a refactoring, not cosmetics. A variable name is part of the interface a reader uses to understand the code. m could mean model, mean, or margin; random_forest_model means one thing. The professor's earlier principle applies here: a reader should never have to ask "what is this variable, and where did it come from?" Explicit names are the cheapest documentation you will ever write.
11.4.7 Worked Example: Bad Design vs. Refactored Design
The professor demonstrated two Python implementations of the same task — training a linear model on four records. The first uses "bad design" with hard-coded magic numbers (e.g., the value 2.5 used as a hard-coded append value). The second is the refactored version that eliminates magic numbers, applies single responsibility, uses proper class structure (data source class, preprocessing class, model class), and demonstrates clean dependency management.
The bad design (sketch). A single script where everything is tangled together:
import pandas as pd
from sklearn.linear_model import LinearRegression
df = pd.read_csv("/data/training_set.csv")
clean = []
for row in df.values:
clean.append(list(row) + [2.5]) # magic number: why 2.5? what is it?
X = [r[:2] for r in clean]
y = [r[2] for r in clean]
model = LinearRegression()
model.fit(X, y)
print(model.predict([[1, 2]])) # untraceable flow, hard-coded path
The refactored design (sketch). The same task, restructured around responsibilities:
class DataSource:
def load(self): # one job: fetch raw data
...
class Preprocessor:
def clean(self, raw): # one job: prepare features
... # no hard-coded append value — the cleaning logic stands alone
class ModelWrapper:
def __init__(self, model): # model injected: dependency inversion
self.model = model
def train(self, X, y): ...
def predict(self, features): ...
Both implementations produce the same prediction output, but the refactored version is maintainable, testable, and scalable. Each class has one responsibility, the model type is injected rather than hard-coded, and the data path lives in configuration.
The professor highlighted one specific example: the magic number 2.5. In the bad design, this number appears as a hard-coded value in the data cleaning function. In the refactored design, it is eliminated — the cleaning logic does not use a hard-coded append value at all. This makes the code more flexible and less error-prone. A magic number is any unexplained literal value that carries hidden meaning; when it changes, you must find every place it appears.
A student asked about when to use classes versus functions:
Q: There are two approaches — class-based and functional. How do we decide which one to use? In industry, good companies use class-based approaches, but as students, we mostly use functional programming.
A: In actual project work, we generally use classes. There are certain points where functions are appropriate, but the recommendation is to use classes in real-world projects. I will share practical code from my current projects that shows where functions are used and where classes are used, so you can understand the distinction from real industry code rather than textbook examples.
Real-world: In industry, class-based approaches are the standard for ML systems. Functions are used for specific, well-scoped tasks within the class structure.
Recap + bridge. Refactoring changes the inside of a system without changing its outside; SOLID principles guide which changes are safe (single responsibility, open/closed, dependency inversion); code smells tell you when to refactor; and the bad-vs-good design demo shows the transformation in practice. This design discipline is exactly what the next topic builds on: wrapping well-designed code in an API so other systems can use it.
11.5 APIs for ML Services
11.5.1 Model as a Service
The concept of "Model as a Service" means wrapping a trained ML model in an API so that other systems can consume its predictions over the network. This is the bridge between a model that works in a notebook and a model that serves real users.
The bridge idea: an API (Application Programming Interface) lets two systems communicate and transfer data without knowing each other's internals. The model is the brain; the API is the mouth and ears — it accepts requests, carries the input to the brain, and returns the answer in a standard format. Without the API, every consumer would have to run your code directly, on their machine, with your environment.
The key architectural principle is decoupling: the model should be developed and maintained separately from the API that exposes it. The model handles prediction logic; the API handles HTTP communication, validation, authentication, and scaling. This separation provides:
- Language independence — The model can be written in Python while the API consumer can be in any language (JavaScript, C#, Java). The API speaks HTTP + JSON, which every language understands.
- Independent scaling — The model and the API can be scaled separately based on their respective loads.
- Seamless model updates — You can update the model (retrain, swap architectures) without changing the API interface, and vice versa.
The professor emphasized this as an important interview question: "Why separate the model from the API?" The answer is flexibility, independent scaling, and the ability to update one without breaking the other.
Real-world: REST (Representational State Transfer) using JSON is the standard for ML inference APIs. REST is an architectural style (defined by Roy Fielding in 2000) that says: represent resources as URLs, and manipulate them with standard HTTP methods — GET retrieves data, POST sends data, PUT updates, DELETE removes. Responses carry status codes: 2xx success (200 OK), 4xx client error (404 Not Found — you asked for something that does not exist), 5xx server error (a bug in the API's code). This is the most common pattern in production ML systems.
11.5.2 Inference Service Architecture
The inference service architecture has key components:
- Endpoint — A URL that accepts requests (typically POST for predictions, GET for health checks).
- Payload — JSON data containing the input features for prediction.
- Validation — Ensuring the input data matches the expected schema before it reaches the model.
- Response — The prediction result returned as JSON.
The request lifecycle. A prediction request travels a fixed path:
- A client (a web app, a mobile app, another service) sends an HTTP request to the endpoint — for example
POST /predictwith a JSON payload of features. - Validation checks the payload against the expected schema before the model ever runs — this is the failing-early principle from Section 11.1 applied at the API boundary.
- Validated features are passed to the model, which computes the prediction.
- The service returns the prediction as a JSON response with a status code.
The input data can come from anywhere — a database, a file system, an external API. The FastAPI service acts as the bridge between the HTTP world and the model.
Scope: the validation step exists because the model is the wrong place to discover bad input. By the time an age of -5 reaches the model, the error is hard to trace back to the caller; at the API boundary, the error is unambiguous — "your request was invalid, here is why." This is the production-world application of failing fast.
11.5.3 FastAPI for ML Services
FastAPI is the recommended framework for ML inference services. Its advantages:
- High performance — Fast execution, with responses in milliseconds to microseconds.
- Auto documentation — The moment you define endpoints with FastAPI, you get automatic API documentation at the
/docsURL. This is invaluable for teams and for testing. - Type safety — Uses Python type hints throughout, catching type errors at development time.
- Asynchronous programming — Supports async/await for handling concurrent requests efficiently.
- Pydantic integration — Uses Pydantic for automated input validation.
The FastAPI skeleton. A minimal inference service:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class CustomerFeatures(BaseModel):
age: int
monthly_bill: float
tenure_months: int
@app.get("/")
def health_check():
return {"status": "ok"}
@app.post("/predict")
def predict(features: CustomerFeatures):
probability = churn_model.predict_proba(
[features.age, features.monthly_bill, features.tenure_months]
)
return {"churn_probability": probability}
The @app.get and @app.post decorators register endpoints; the type annotations on CustomerFeatures drive both validation and the automatic /docs page. FastAPI is a wrapper around your code that makes your functions available to other people in a standardized format.
The professor's emphasis: develop the model separately, develop the API separately, then integrate them. Take care of the model in terms of stability and accuracy. Take care of the API in terms of stability and scalability. When you integrate them, you get the right GET/POST responses. The same discipline applies to the logic layer: keep the code that does the calculation separate from the API code, so you can swap frameworks or test logic without touching endpoints.
11.5.4 Validation with Pydantic
Pydantic provides automated input validation that stops invalid data before it reaches the model. The professor's example: ensure that the age field is always an integer between 0 and 120, preventing runtime crashes from nonsensical inputs like negative ages or ages over 120.
Declaring the rules. Pydantic models declare the expected shape of the payload, and FastAPI enforces it automatically:
from pydantic import BaseModel, Field
class CustomerFeatures(BaseModel):
age: int = Field(ge=0, le=120) # age must be an integer between 0 and 120
monthly_bill: float = Field(ge=0.0) # a bill cannot be negative
tenure_months: int = Field(ge=0)
If a client sends {"age": -5, ...}, the request is rejected with an error response — the payload never reaches the model. The rule lives in one place, is visible in the auto-generated docs, and is enforced on every request with no hand-written if checks.
This is Pydantic's role in the pipeline: the schema is the contract between the API and its consumers, and the validation is the failing-early filter from Section 11.1 moved to the network boundary.
11.5.5 Worked Example: FastAPI Churn Prediction Service
The professor demonstrated a FastAPI service wrapping a customer churn prediction model (the same churn model from a previous lecture, now enhanced with FastAPI). The service exposes:
- A health check endpoint (
GET /) to verify the service is running. - A predict endpoint (
POST /predict) that accepts customer features (age, monthly bill, tenure months) and returns churn probability.
The demonstration, scenario by scenario.
Scenario 1 — Valid input. A request arrives with age 34, plus monthly bill and tenure months:
POST /predict
{"age": 34, "monthly_bill": 89.5, "tenure_months": 12}
Validation passes, the model runs, and the service returns the churn probability successfully (e.g., {"churn_probability": 0.41}) with a 200 status code.
Scenario 2 — Invalid input (negative age). The same endpoint receives:
POST /predict
{"age": -5, "monthly_bill": 89.5, "tenure_months": 12}
Pydantic rejects the request before the model ever runs. The response is a "Unprocessable Content" error (HTTP 422) with the message "Input should be greater than 17." — the boundary defined in the model's validation, enforced automatically. The model was never called; the caller gets an immediate, precise explanation of what is wrong with their payload.
The professor noted that the auto-generated documentation at /docs allows anyone to interact with the API directly from the browser — entering values, clicking "Try it out," and seeing responses. This documentation is generated automatically from the code's type hints and Pydantic models, which is why the type-safety and Pydantic features pay off twice: they catch errors at development time and produce the docs for free.
Exam note: the concept of decoupling model from API, and why this separation matters for scaling and maintainability, is a common interview and exam topic. Know the three benefits: language independence, independent scaling, seamless updates.
Recap + bridge. Model as a Service turns a trained model into a network-accessible product: the API wraps the model (decoupling), endpoints carry requests and responses, FastAPI provides the framework, and Pydantic validates at the boundary — failing early, in API form. The lecture closes by pointing forward: version control (Git) and packaging (Docker) are what let you share and deploy this service reliably — the topic of the next session.
11.6 Version Control and Packaging — Introduction
11.6.1 Git and Docker Overview
The lecture concluded with a brief introduction to version control and packaging, noting that Git and Docker would be covered in detail in the next session (Session 12). The professor mentioned that webinars have already covered Docker and Git in detail, and the next class will build on that foundation with practical directory structure and workflow examples.
The two questions this topic answers. Version control answers: "What changed, when, and who changed it — and can we undo it?" Packaging answers: "Will it run the same way on my machine, your machine, and the production server?" Together they are what make collaborative, dependable software possible — everything this lecture has built toward ships through Git and runs inside Docker.
The key concept introduced: version control (Git) tracks changes to code over time, enabling collaboration, rollback, and audit trails. Packaging (Docker) encapsulates the entire runtime environment — code, dependencies, system libraries — into a container that runs consistently across different machines and environments.
How Git thinks. Git stores snapshots of your project. A repository is a directory where Git tracks changes; a commit saves a snapshot with a unique ID, author, timestamp, and message; a remote repository (e.g., on GitHub) lets a team share commits via push and pull; branches isolate work in progress so multiple people can change code without overlap, and merge combines the results. Every commit is a rollback point — a change that causes a bug can be undone by returning to an earlier commit.
What Docker packages. A Docker container bundles the application, its dependencies (Python libraries, versions), and system libraries into a single unit. The image is built from a recipe (a Dockerfile) and runs identically everywhere — no "works on my machine" differences between a developer's laptop, the staging server, and production. The container also carries the environment, which directly addresses the hidden-state problems from Section 11.3: the code runs against the same libraries, in the same order, wherever it runs.
Scope — what Git is not for. Git is designed for code, not data. It is not meant to back up or version large datasets — the file sizes cause problems, and storing data in repositories raises duplication and security risks. Data belongs in a dedicated system; code belongs in Git.
Real-world: Docker containers solve the "it works on my machine" problem by ensuring that the production environment is identical to the development environment. In industry, CI/CD pipelines (Section 11.2) run inside containers, and every deployment is a version-controlled artifact: the code commit, the lint gate, the tests, and the container image form one auditable chain.
Recap + bridge. This lecture's journey in four acts: make systems fail loudly (logging and exceptions), enforce discipline automatically (formatting and linting), separate exploration from delivery (research vs. production), design for change (refactoring and SOLID), and expose models over the network (APIs). Git and Docker are the delivery mechanism for all of it — and Session 12 covers them in practical detail with directory structures and workflow examples.
11.7 Student Questions and Answers
11.7.1 Exchanges on Error Handling, Logging, Formatting, and Design
Q: I understand the concept of failing early. It makes a lot of sense, but practically, how do we ensure that?
A: Failing early can come from many angles. The first thing is how well you handle your data. You get raw data with noise — numbers where strings are expected, special characters in fields. You can put a filter at an early stage so that bad data gets filtered out immediately. When we run models, we always make sure clean data gets passed first. This is the concept of robustness — catching the failure early. In a business context, you discuss with stakeholders from a design perspective: "We will filter and work only on the five profiles important to your business." The implementation behind that — classes, data models, SQL filters — is your technical decision.
Q: Are there scenarios beyond data type mismatch that we need to anticipate for early failure?
A: Yes, there are many scenarios. The exceptions shown are Python-specific, but the conceptual design principle is language-agnostic. When you move from coding to designing, you think about systems, not just code. When you go to a meeting with a customer, they give you requirements in business terms, not code terms. You answer them from a design and architectural perspective, which enhances your knowledge and helps you grow as a business person.
Q: In a typical ML system, how often or at what positions do we need to add logging lines? Is it necessary to put a logger at every function?
A: Put your loggings at the stages where you are applying your core business concepts, not at every coding line. The "core concept" means the business requirement, not the coding logic. When a business person asks questions, they ask at the requirement level — "stage 4" in their workflow — not at every function call. So put loggers at the business logic level. If you are using the divide-and-conquer debugging strategy, you can also put logging at module levels for small, isolated blocks.
Q: When you explain logging for modules, does OOP (classes with functions) help with stage-wise logging?
A: Yes, but it depends on the technology. In data warehousing or ETL contexts, defining stages is not always straightforward because the work is block-by-block. The approach of logging at function levels within classes applies to most Python projects, but not universally across all technologies.
Q: Does Ruff make changes to the code when combined with Git operations?
A: Yes, it does. Ruff ensures function arguments have proper spacing, that code follows PEP 8 standards. You can run ruff check --fix locally to auto-fix issues. In practice, this is typically caught in CI/CD pipelines — if the code is not properly linted, the pipeline fails, and you must fix and recommit.
Several students asked variants of one question about code organization — whether class-based or functional code is better and how to choose:
Q: There are two approaches — class-based and functional. How do we decide which one to use? And based on what you shared, is class-based code better than functional code?
A: In actual project work, we generally use classes. There are certain points where functions are appropriate, but the recommendation for real-world projects is classes — in the project world, classes are generally recommended, though the choice depends on the specific scenario. Practical code from industry will be shared to illustrate where each approach is used, so you can understand the distinction from real industry code rather than textbook examples.
11.8 Exam Guidance Summary
11.8.1 Assessment-Relevant Topics and Study Advice
Assessment checklist for this lecture:
- Common Python exceptions — Understanding the definitions and descriptions of common Python exceptions (
ValueError,KeyError,TypeError,IndexError) is assessment-relevant. Be ready to match each exception to a realistic data science scenario. - SOLID principles — The SOLID principles are tied to the textbook and are exam-relevant. Know the three covered in depth: Single Responsibility, Open/Closed, and Dependency Inversion — their names, meanings, and the professor's phrasing.
- Logging levels — Understanding the difference between logging levels (
DEBUG,INFO,WARNING,ERROR,FATAL) and when to use each in production vs. non-production environments is important. Remember: production typically runs atERROR, non-production atWARNINGorINFO. - Model–API decoupling — The concept of decoupling model from API, and why this separation matters for scaling and maintainability, is a common interview and exam topic. Know the three benefits: language independence, independent scaling, seamless model updates.
- Code smells and refactoring — Code smells (long functions, duplicate logic, God objects, hard-coded paths) and refactoring techniques are important for both exams and practical work. Note the professor's emphasis: long functions and hard-coded paths are the two most important smells.
- Interview answer — If asked "What are the most important things when deploying code to production?", the answer is stability and scalability.
Study advice from the lecture:
- Students should go through the demonstrated code files after class — the robust error-handling and logging demonstration (Section 11.1.10) and the bad-vs-refactored design example (Section 11.4.7) — and try to integrate the patterns into their own code. Both demos are examinable in the sense that their patterns (three-level severity mapping, magic-number elimination, single responsibility) are the material to reproduce, not the exact code.
- For the error-handling demo, test yourself by changing the inputs: what log level fires when the vector is too short? When it contains NaN? When it is clean?
- For the FastAPI demo (Section 11.5.5), be able to explain step by step what happens to a request — endpoint → validation → model → response — including why a negative age returns an error before the model is ever called.
11.9 Key Industry Applications
11.9.1 Tools, Platforms, and Practices Referenced
- AWS CloudWatch and Elasticsearch — Observability platforms for persistent logging in production ML systems. Logs written by the Python
loggingmodule are piped to these platforms so stakeholders can query what happened, when, and with which parameters. - FastAPI — High-performance Python framework for ML inference services with auto-documentation (at
/docs) and Pydantic validation. - Docker — Containerization platform that solves environment consistency across development, staging, and production — the fix for "it works on my machine."
- Git — Version control system for tracking code changes and enabling collaboration, rollback, and audit trails.
- CI/CD pipelines — Automated pipelines that enforce linting (Ruff), formatting (Black), and testing before code reaches production; a commit that fails the lint step is blocked and must be fixed and recommitted.
- ServiceNow — Enterprise incident management platform referenced in the context of user-reported issues when logging is not enough — the "users become your loggers" failure mode made concrete.
- AWS EC2 — Cloud compute instance used as the example for long-running ML training jobs (the 48-hour job) where persistent logging is critical because sessions disconnect.
- ETL (Extract, Transform, Load) — Data warehousing practice where early data validation (failing early) is a standard pattern — the first stage of any ETL pipeline is cleaning raw data.
- Pydantic — Python library for data validation used in FastAPI to stop invalid inputs before they reach the model (e.g., age must be an integer between 0 and 120).
- pdb and VS Code debugger — Debugging tools for stepping through code: pdb for the command line, the IDE debugger for breakpoint-based inspection in development environments.
- PEP 8, Black, Ruff, Mypy, isort - The formatting and static-analysis toolchain: the style standard, the formatting tool, the linter, the type checker, and the import sorter.
SEML Lecture 11 notes
Sections Breakdown
Failing early and loudly: robustness, common Python exceptions, professional try-except handling, traceback reading, custom exceptions, and leveled logging vs print.
PEP 8 style, the Black formatter, the Ruff linter, and type hints with Mypy — moving style from opinion to automation.
The two worlds of ML code: notebook experimentation vs stable, scalable production systems, and the hidden state trap of the transition.
Refactoring without changing external behavior, the SOLID principles for ML, modularity, dependency inversion, code smells, and actionable techniques.
Model as a Service: decoupling model from API, the inference service architecture, FastAPI endpoints, and Pydantic validation at the boundary.
Git for version control and Docker for packaging — the delivery mechanism for dependable ML software, previewed before Session 12.
Consolidated Q&A exchanges on failing early, where to place loggers, OOP and stage-wise logging, Ruff with Git, and classes vs functions.
Assessment-relevant clusters: exceptions, SOLID principles, logging levels, model-API decoupling, code smells and refactoring, and the stability-and-scalability interview answer.
The tools and platforms named in the lecture: CloudWatch, Elasticsearch, FastAPI, Docker, Git, CI/CD, ServiceNow, EC2, ETL, Pydantic, and the Python toolchain.
Exam Revision Notes
Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.
Error Logging and Debugging
Must-know: Common Python exceptions (ValueError, KeyError, TypeError, IndexError), the five logging levels and when to use each in production (ERROR) vs non-production (WARNING/INFO), and the fail-early principle.
Top pitfall: Bare except clauses create a zombie state — the system keeps running but the error information is lost; catch specific exceptions instead. Read tracebacks from bottom to top, not top to bottom.
Self-check: A model expects 3 features and receives [2.5, 3.1]. What log level fires and why?
Connects to: 11.2, 11.4
Code Formatting and Linting
Must-know: PEP 8 conventions (snake_case variables/functions, PascalCase classes, 4-space indent, import groups), what Black vs Ruff vs Mypy each do, and that Ruff can modify code (ruff check --fix) with CI/CD enforcement.
Top pitfall: Inconsistent linting: each linter uses a slightly different style guide, so teams must pick one linter and stick to it; mixing tools recreates style debates.
Self-check: A function is annotated to accept float but receives a str at a call site. Which tool flags this without running the code?
Connects to: 11.1, 11.3
Research Versus Production Code
Must-know: The four dimensions (goal, environment, priority, state) and the feature-by-feature comparison (unit of work, error handling, testing, configuration, state); interview answer: stability and scalability are the most important things when deploying to production.
Top pitfall: Out-of-order notebook execution and hidden global state break when the code runs top-to-bottom in production; move verified logic into .py modules as soon as an experiment succeeds.
Self-check: A notebook runs cell 8 before cell 3 and works. Why does the same logic fail as a script in production?
Connects to: 11.1, 11.4, 11.6
Designing and Refactoring
Must-know: Definition of refactoring (external behavior unchanged), the three SOLID principles covered (S, O, D) with professor's phrasing, the four code smells, and the three refactoring techniques; class-based over functional in industry.
Top pitfall: Magic numbers like 2.5 hard-coded in cleaning logic, and duplicated logic that forces the same fix in every copy — hard-coded paths make the system brittle.
Self-check: A module must return output in 2 seconds with ages 15-30 as parameters. You rewrite its internal algorithm. What must stay unchanged?
Connects to: 11.1, 11.3, 11.5
APIs for ML Services
Must-know: Why separate model from API (language independence, independent scaling, seamless updates); the inference service architecture (endpoint, payload, validation, response); FastAPI advantages; Pydantic validation example (age 0-120, 422 Unprocessable Content).
Top pitfall: Hard-coupling model and API means every model retrain or API change breaks the other; validation must happen at the API boundary so bad payloads like negative age never reach the model.
Self-check: A request sends age = -5 to /predict. What happens, and why does the model never see it?
Connects to: 11.1, 11.4, 11.6
Version Control and Packaging — Introduction
Must-know: Git = version control (tracking changes, collaboration, rollback); Docker = packaging the runtime environment into containers for consistency across machines; detailed coverage in Session 12.
Top pitfall: Treating Git as a data backup system — it is designed for code, not large datasets; storing data in repositories creates duplication and security risks.
Self-check: What problem do Docker containers solve, and how?
Connects to: 11.3, 11.2
Student Questions and Answers
Must-know: Loggers belong at business-logic stages (where business people ask questions), not at every function; failing early is a design-level, language-agnostic principle; classes are the industry standard with functions for well-scoped tasks.
Top pitfall: Adding a logger at every function call drowns the business-relevant signals in noise; place loggers where core business concepts are applied.
Self-check: Where should loggers go in an ML system, and why?
Connects to: 11.1, 11.4
Exam Guidance Summary
Must-know: Five assessment-relevant clusters: exceptions, SOLID (S/O/D), logging levels, model-API decoupling, code smells + refactoring; production = stability and scalability.
Top pitfall: Studying the demo code by rote instead of understanding the patterns (severity mapping, magic-number elimination, single responsibility) the demos illustrate.
Self-check: Name the five assessment-relevant topic clusters from this lecture.
Connects to: 11.1, 11.2, 11.4, 11.5
Key Industry Applications
Must-know: Named industry tools and what each does: CloudWatch/Elasticsearch (log observability), FastAPI (inference APIs), Pydantic (input validation), Docker (environment consistency), Git (version control), CI/CD (enforced quality gates).
Top pitfall: Mixing up tool roles — Black formats, Ruff lints/fixes, Mypy type-checks, isort sorts imports; each occupies a different niche.
Self-check: Which platform receives Python logging output for querying in production?
Connects to: 11.1, 11.5, 11.6