Skip to main content

Python Classes and Objects

Detailed Core Lesson

This is an original, detailed Python core lesson. It is designed as a practical reference page for learners who want more depth than a short example.

Learning Goal

Define classes, initialize objects, use methods, attributes, inheritance, and string representations.

This lesson belongs to 40. Errors, Input, Files, Classes, and MySQL. Read it as both a tutorial and a practical checklist: understand the concept, run the examples, change the examples, then connect the topic to automation, AI scripts, and server work.

Mental Model

Python programs are built from values, names, expressions, statements, and blocks. A beginner mistake is memorizing syntax without understanding what data is flowing through the program.

For Python Classes and Objects, always ask:

  • What values are being created?
  • What names refer to those values?
  • What operation transforms the values?
  • What output, return value, exception, file, database row, or side effect is produced?
  • What should happen when the input is missing, empty, invalid, or unexpected?

Basic Syntax Pattern

def run_04_python_classes_example() -> None:
topic = "Python Classes and Objects"
print(f"Learning: {topic}")


run_04_python_classes_example()

Key points:

  • def creates a function.
  • Indentation groups the function body.
  • topic is a variable name.
  • print() sends output to the console.
  • The final line calls the function.

Step-by-Step Example

def explain_value(value: object) -> str:
value_type = type(value).__name__
return f"value={value!r}, type={value_type}"


examples = ["python", 42, 3.14, True, None]

for item in examples:
print(explain_value(item))

What this demonstrates:

  • Python values have types.
  • Functions can accept values and return new values.
  • for loops repeat work for each item in a collection.
  • !r formats a value using its developer-oriented representation.

Practical Use Cases

ContextHow this topic is used
Beginner scriptsBuild confidence with small examples and visible output
AutomationProcess files, command results, API responses, and reports
AI workflowsPrepare prompts, validate model output, and transform datasets
Server workInspect logs, services, configs, health checks, and database rows
Data tasksClean, reshape, filter, summarize, and export information

Expanded Example for Real Work

from pathlib import Path


def write_report(lines: list[str], output_file: Path) -> None:
output_file.parent.mkdir(parents=True, exist_ok=True)
text = "\n".join(lines) + "\n"
output_file.write_text(text, encoding="utf-8")


report_lines = [
"Python core practice report",
"Topic: Python Classes and Objects",
"Status: completed",
]

write_report(report_lines, Path("practice-output") / "report.txt")

This pattern is intentionally simple but useful. Many production scripts do the same basic things: build a list of messages, create a folder, write a file, and make the result easy to inspect.

Common Variations

VariationExampleWhen to use it
Print a valueprint(value)Quick learning or debugging
Return a valuereturn resultReusable functions and tests
Save a valuepath.write_text(text)Reports, generated files, logs
Validate a valueif not value: raise ValueError(...)User input, config, API responses
Convert a valueint(text)Input parsing and typed operations

Common Mistakes

MistakeWhy it causes troubleBetter approach
Running examples without modifying themYou may recognize code without understanding itChange names, values, and inputs
Ignoring typesOperations fail when values are not what you expectPrint type(value) while learning
Hiding too much in one lineDebugging becomes harderBreak work into named steps
Catching every exception silentlyFailures become invisibleLog or print a useful error message
Trusting user input directlyBad input crashes or corrupts outputValidate and convert input explicitly

Debugging Checklist

Use this checklist when an example does not work:

  1. Check indentation first.
  2. Read the final line of the traceback.
  3. Find the exact file and line number in the traceback.
  4. Print the value and type of suspicious variables.
  5. Reduce the example to the smallest failing version.
  6. Add one line back at a time until the failure returns.

Debug helper:

def debug_value(name: str, value: object) -> None:
print(f"{name}={value!r} type={type(value).__name__}")

Practice Exercises

  1. Copy the basic syntax pattern into a new .py file and run it.
  2. Rename the function and variable names to your own examples.
  3. Add three more example values.
  4. Print both the value and its type.
  5. Wrap a failing conversion in try and except.
  6. Write a short report file into a temporary directory.
  7. Explain the code out loud line by line.

Mini Project

Create a file called core_practice.py that:

  • Prints the current topic name.
  • Creates at least five values of different types.
  • Shows each value and type.
  • Writes a short summary to practice-output/summary.txt.
  • Handles at least one expected error with a clear message.

Starter structure:

from pathlib import Path


def main() -> int:
output = Path("practice-output")
output.mkdir(exist_ok=True)
print("Practicing Python Classes and Objects")
return 0


if __name__ == "__main__":
raise SystemExit(main())

Review Questions

  • What is the smallest valid example of this topic?
  • What values does the example create?
  • Which operations can fail?
  • What should be printed for a beginner to understand the result?
  • How would this topic appear in a file-processing script?
  • How would this topic appear in an AI prompt or response-validation script?
  • How would this topic appear in a server health-check script?

Quick Reference

value = "example"
print(value)
print(type(value))
print(repr(value))

Use print() for learning. Use logging when building real tools that other people or schedulers will run.

Extended Examples

Example 1: Parameterized Validation

from dataclasses import dataclass
from typing import Optional


@dataclass(frozen=True)
class ValidationResult:
valid: bool
value: Optional[str] = None
error: Optional[str] = None


def validate_and_normalize(
value: str,
min_length: int = 1,
max_length: int = 100,
strip_whitespace: bool = True
) -> ValidationResult:
if strip_whitespace:
value = value.strip()

if not value:
return ValidationResult(valid=False, error="empty value after processing")

if len(value) < min_length:
return ValidationResult(valid=False, error=f"too short: {len(value)} < {min_length}")

if len(value) > max_length:
return ValidationResult(valid=False, error=f"too long: {len(value)} > {max_length}")

return ValidationResult(valid=True, value=value)


# Test different scenarios
test_cases = [" hello ", "a", "x" * 200, "", "validinput"]
for case in test_cases:
result = validate_and_normalize(case)
print(f"input={case!r} -> valid={result.valid}, error={result.error}")

Example 2: Structured Result with Context

from dataclasses import dataclass
from datetime import datetime
from enum import Enum


class Status(Enum):
SUCCESS = "success"
FAILURE = "failure"
PARTIAL = "partial"


@dataclass(frozen=True)
class OperationResult:
status: Status
message: str
timestamp: datetime
details: dict

def is_success(self) -> bool:
return self.status == Status.SUCCESS

def to_dict(self) -> dict:
return {
"status": self.status.value,
"message": self.message,
"timestamp": self.timestamp.isoformat(),
"details": self.details,
}


def run_operation(value: str) -> OperationResult:
if not value or len(value) < 3:
return OperationResult(
status=Status.FAILURE,
message="value too short",
timestamp=datetime.now(),
details={"value_length": len(value) if value else 0}
)

return OperationResult(
status=Status.SUCCESS,
message="operation completed",
timestamp=datetime.now(),
details={"processed_length": len(value)}
)


result = run_operation("test")
print(result.to_dict())

Integration Patterns

Combining with argparse

import argparse
from dataclasses import dataclass


@dataclass(frozen=True)
class Config:
input_value: str
verbose: bool
output_file: str


def parse_args() -> Config:
parser = argparse.ArgumentParser(description="Process values with validation")
parser.add_argument("input_value", help="Value to process")
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
parser.add_argument("-o", "--output", default="output.txt", help="Output file")
args = parser.parse_args()
return Config(
input_value=args.input_value,
verbose=args.verbose,
output_file=args.output
)


def main() -> int:
config = parse_args()
if config.verbose:
print(f"Processing: {config.input_value}")
print(f"Output to: {config.output_file}")
print(f"Result: {config.input_value.upper()}")
return 0


if __name__ == "__main__":
raise SystemExit(main())

Combining with logging

import logging
from dataclasses import dataclass


logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(message)s"
)
LOG = logging.getLogger(__name__)


@dataclass(frozen=True)
class LoggedResult:
topic: str
success: bool
detail: str


def process_with_logging(value: str) -> LoggedResult:
LOG.debug(f"Processing value: {value}")

if not value:
LOG.warning("Empty value received")
return LoggedResult(topic="process", success=False, detail="empty")

result = value.strip().upper()
LOG.info(f"Processed to: {result}")
return LoggedResult(topic="process", success=True, detail=result)


result = process_with_logging("hello")
LOG.debug(f"Final result: {result}")

Operational Considerations

When using this pattern in production environments:

  1. Environment Variables: Read configuration from environment variables with sensible defaults
  2. Exit Codes: Return non-zero exit codes when operations fail
  3. Error Messages: Write useful error messages to stderr, not stdout
  4. Dry Runs: Support --dry-run flag for state-changing operations
  5. Logging: Use structured logging with appropriate log levels

Production Snippet

import os
import sys
import logging
from pathlib import Path


LOGGING_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper()
logging.basicConfig(
level=getattr(logging, LOGGING_LEVEL, logging.INFO),
format="%(asctime)s %(levelname)s %(name)s: %(message)s"
)
LOG = logging.getLogger(__name__)


def get_working_directory() -> Path:
"""Get working directory with validation."""
cwd = os.environ.get("WORK_DIR")
if cwd:
path = Path(cwd)
if path.is_dir():
return path
LOG.warning(f"WORK_DIR not a directory: {cwd}")
return Path.cwd()


def main() -> int:
LOG.info("Starting application")
work_dir = get_working_directory()
LOG.info(f"Working directory: {work_dir}")

try:
# Main logic here
pass
except Exception as e:
LOG.error(f"Failed: {e}")
return 1

LOG.info("Completed successfully")
return 0


if __name__ == "__main__":
raise SystemExit(main())

Extended Troubleshooting

ProblemLikely CauseSolution
Function not foundImport error or typo in nameCheck imports and function definition
Type error on resultWrong return type or None handlingAdd type hints and handle None
File not foundWrong path or permissionsUse absolute paths and check permissions
Tests failEdge cases not handledAdd more test cases and validation
Works locally, fails on serverEnvironment differencesCheck Python version, paths, and dependencies

Debugging Steps

  1. Run with -v or --verbose flag
  2. Print the input values and types
  3. Check the Python version: python3 --version
  4. Verify dependencies: python3 -c "import module_name"
  5. Run with dry-run mode if available
  6. Check logs for error messages

Additional Practice

  1. Modify the core pattern to accept multiple values
  2. Add a retry mechanism for failed operations
  3. Implement a config file loading pattern
  4. Add a simple progress indicator for long operations
  5. Create a CLI with subcommands using argparse

Field Notes

This pattern forms the foundation of reliable Python automation. The key principles:

  • Explicit is better than implicit - Name variables and functions clearly
  • Errors should never pass silently - Handle failures explicitly
  • Readability counts - Code is read more than written
  • Flat is better than nested - Avoid deep nesting
  • Small is better - Many small functions are easier to test

Build confidence by applying this pattern to real automation tasks, then expand to handle more complex scenarios.

Summary

  • Validate early, validate often
  • Return structured results, not just values
  • Log usefully without overwhelming
  • Handle errors explicitly
  • Test the failure paths, not just success