VALLEYS & BYTES

Python OOP · Learning Guide · Narra Book Haven

Source Code
Explained
Book Store Edition

A complete walkthrough of a Python bookstore management system — every class, every method, every pattern dissected through three lenses: why it was written, why it matters, and exactly how it works. Extended with OOP theory, design patterns, a Python cheat sheet, and practice challenges.

Python 3.x OOP · Classes · Methods datetime · typing Beginner → Intermediate ~50 books catalogued
bookstore.py — Full Source (Condensed)
from datetime import datetime
from typing import Dict, Optional

class Book:
    """Represents a single book in the bookstore inventory."""

    def __init__(self, title: str, author: str, price_php: float, quantity: int):
        self.title     = title.strip()
        self.author    = author.strip()
        self.price_php = round(float(price_php), 2)
        self.quantity  = max(0, int(quantity))

    def __str__(self) -> str:
        return f"{self.title:<45} | {self.author:<30} | ₱{self.price_php:>8,.2f} | Stock: {self.quantity:>2}"

    def can_sell(self, qty: int) -> bool:
        return 0 < qty <= self.quantity

    def sell(self, qty: int):
        self.quantity -= qty

class Bookstore:
    """Manages the bookstore inventory and sales."""

    def __init__(self, name: str = "Narra Book Haven"):
        self.name      = name
        self.books: Dict[str, Book] = {}
        self.sales_log: list = []

    def add_book(self, title: str, author: str, price_php: float, quantity: int = 15) -> None:
        key = title.strip().lower()
        if key in self.books:
            self.books[key].quantity += max(0, quantity)
        else:
            self.books[key] = Book(title, author, price_php, quantity)

    def find_book(self, search_term: str) -> Optional[Book]:
        """Supports searching by Title or Row Number."""
        search_term = search_term.strip().lower()
        if search_term.isdigit():
            idx = int(search_term) - 1
            all_books = list(self.books.values())
            if 0 <= idx < len(all_books):
                return all_books[idx]
        return self.books.get(search_term)

    def purchase(self, customer_name: str, title_or_idx: str, quantity: int) -> None:
        book = self.find_book(title_or_idx)
        if not book:
            print(f"\n  [!] '{title_or_idx}' not found.")
            return
        if quantity < 1:
            print("\n  [!] Quantity must be at least 1.")
            return
        if not book.can_sell(quantity):
            print(f"\n  [!] Only {book.quantity} copies left.")
            return
        total     = book.price_php * quantity
        book.sell(quantity)
        timestamp = datetime.now().strftime("%Y-%m-%d %I:%M %p")
        self.sales_log.append({
            "time": timestamp, "customer": customer_name,
            "title": book.title, "qty": quantity, "total": total
        })
        print(f"\n  ✅ Purchase successful!")
        print(f"  Customer: {customer_name} | Total: ₱{total:,.2f}")

def main():
    store = Bookstore("Narra Book Haven")
    store.populate_with_sample_books()
    print("="*90)
    print(f"{store.name:^90}")
    print("="*90)
    while True:
        print("\n[1] View All Books  [2] Buy a Book  [3] Exit")
        choice = input("Enter Choice: ").strip()
        if choice == "1":
            store.list_available_books()
        elif choice == "2":
            store.list_available_books()
            target = input("Enter Title or Number (#): ").strip()
            try:
                qty = int(input("How many copies? : "))
            except ValueError:
                print("  Invalid quantity."); continue
            customer = input("Your name: ").strip() or "Guest"
            store.purchase(customer, target, qty)
        elif choice == "3":
            total_rev = sum(s['total'] for s in store.sales_log)
            print(f"\nTotal Revenue: ₱{total_rev:,.2f}")
            print(f"Thank you for visiting {store.name}!")
            break

if __name__ == "__main__":
    main()
Python 3 — Output
Press "Run Code" to execute the script...
00 —

What is Object-Oriented Programming?

Before dissecting the code, it helps to understand the thinking behind OOP. Every line in this program was shaped by four fundamental principles — the pillars that define how professional Python code is structured.

🔒

Encapsulation

Each class bundles its data and the methods that operate on it. Book guards its price and quantity behind its own methods — external code cannot reach in and corrupt them directly.

🧬

Abstraction

Callers don't need to know how purchase() validates or logs — they just call it and trust it. Complexity is hidden behind a simple, clean interface.

🔗

Separation of Concerns

Book knows about books. Bookstore knows about inventory. main() knows about the user. Each layer does exactly one job and does it well.

🐍
Why Python for OOP? Python treats everything as an object — integers, functions, even modules. Its clean syntax makes OOP ideas clear without ceremony. You focus on the logic, not the boilerplate.
1

Define the blueprint — the class

A class is a template. It describes what data (attributes) an object holds and what actions (methods) it can perform. class Book: is the blueprint; each book in your inventory is an instance of that blueprint.

2

Construct instances with __init__

When you write Book("Dune", "Herbert", 590, 15), Python calls __init__ automatically — it is the factory that stamps out a fresh object and stores its data in self.

3

Operate through methods

Methods (can_sell, sell, add_book) are functions that live inside a class. They always receive self — a reference to the instance they belong to — so they can read and modify its data.

4

Compose classes together

Bookstore contains a dictionary of Book objects. One class holding instances of another is called composition — the most common way to build complex systems from simple, tested pieces.


01 —

The Imports

Lines 1–2 bookstore.py
from datetime import datetime
from typing import Dict, Optional

① Why used

datetime stamps every sale with the exact moment it occurred. Dict and Optional are type-annotation helpers — they let the programmer declare precisely what kind of data each variable holds or a function returns.

② Significance

Without datetime there is no sales timestamp — every transaction becomes anonymous in time. Without the typing imports, the code runs identically but becomes far harder to read, debug, and maintain as the codebase grows.

③ How it operates

datetime.now() captures the live system clock at the moment a sale is made. Dict[str, Book] annotates a dictionary whose keys are strings and values are Book objects. Optional[Book] signals a function returns either a Book or None.

💡
Python 3.9+ note: In modern Python you can write dict[str, Book] and Book | None directly — the typing module helpers are no longer required. The code uses them for compatibility with Python 3.7+.

02 —

The Book Class

The Book class is the atomic unit of the entire system. It models a single real-world book and contains everything needed to describe, display, and sell it.

Constructor — __init__ Lines 7–11

Book.__init__
def __init__(self, title: str, author: str, price_php: float, quantity: int):
    self.title     = title.strip()
    self.author    = author.strip()
    self.price_php = round(float(price_php), 2)
    self.quantity  = max(0, int(quantity))

① Why used

This is the blueprint for creating any book object. Every book in the inventory is built through this constructor. It also silently sanitises incoming data — bad input gets corrected rather than crashing the program.

② Significance

No book object can exist without passing through here. It is the single point of data integrity for the entire inventory — enforcing clean titles, valid prices, and non-negative stock counts from the very moment a book is created.

③ How it operates

.strip() removes stray whitespace from names. round(..., 2) locks prices to exactly 2 decimal places. max(0, int(quantity)) silently converts negatives to zero — passing -5 becomes 0.

Display Method — __str__ Lines 13–15

Book.__str__
def __str__(self) -> str:
    return f"{self.title:<45} | {self.author:<30} | ₱{self.price_php:>8,.2f} | Stock: {self.quantity:>2}"

① Why used

Python calls this automatically whenever a Book object is passed to print(). It turns raw data into a formatted table row so the user sees a neat, aligned catalogue — not a raw memory dump.

② Significance

This is the entire visual interface for the inventory. Without it, print(book) would output <Book object at 0x7f…> — useless to a customer browsing titles.

③ How it operates

Format codes inside the f-string do all the column alignment. Each code sets a fixed field width, ensuring every row lines up perfectly regardless of title length.

Format CodeMeaningExample Output
:<45Left-align, pad to 45 charsThe Kite Runner …
:<30Left-align, pad to 30 charsKhaled Hosseini …
:>8,.2fRight-align, 8 wide, comma thousands, 2 decimals 425.75
:>2Right-align stock in 2 chars18

The Gatekeeper Pair — can_sell & sell

Book.can_sell / Book.sell
def can_sell(self, qty: int) -> bool:
    return 0 < qty <= self.quantity

def sell(self, qty: int):
    self.quantity -= qty

① Why used

The two methods are deliberately separated — one checks whether a sale is valid, the other executes it. Stock can never be deducted without first confirming availability.

② Significance

can_sell is the safety lock. sell is the trigger. You cannot fire the trigger without first passing the lock — preventing overselling regardless of how complex the calling code becomes.

③ How it operates

can_sell returns True only when qty > 0 and doesn't exceed current stock. If 5 copies remain and a customer requests 6, it returns False. sell then simply subtracts the sold number.

🔑
Python dunder methods — the complete family: __init__ (constructor), __str__ (string representation for print()), __repr__ (developer representation), __len__ (for len()), __eq__ (for == comparisons), __lt__ (for < / sorting). Implementing dunders makes your class behave like a native Python type.

03 —

The Bookstore Class

The Bookstore class is the manager of the entire system. It owns the inventory dictionary and sales log, and exposes all the operations a real bookstore needs.

Bookstore.__init__
def __init__(self, name: str = "Narra Book Haven"):
    self.name       = name
    self.books: Dict[str, Book] = {}
    self.sales_log: list = []

① Why used

books is a dictionary — O(1) average lookup by key — while sales_log is an append-only list that grows chronologically with every purchase.

② Significance

A dictionary keyed by lowercased title makes find_book() blazing fast even with thousands of entries. A list for sales preserves insertion order, making session revenue easy to compute.

③ How it operates

The default parameter name="Narra Book Haven" means Bookstore() works without arguments. Dict[str, Book] is a type hint: string keys mapping to Book objects. sales_log starts empty and grows with each purchase.

add_book — Upsert Pattern

Bookstore.add_book
def add_book(self, title, author, price_php, quantity=15) -> None:
    key = title.strip().lower()
    if key in self.books:
        self.books[key].quantity += max(0, quantity)   # restock
    else:
        self.books[key] = Book(title, author, price_php, quantity)  # new
📖
The Upsert Pattern: "Upsert" = Update if exists, Insert if new. This is one of the most common patterns in database and inventory code — one method handles both cases cleanly, eliminating the need for callers to check first.

find_book — Dual Search

Bookstore.find_book
def find_book(self, search_term: str) -> Optional[Book]:
    search_term = search_term.strip().lower()
    if search_term.isdigit():
        idx = int(search_term) - 1
        all_books = list(self.books.values())
        if 0 <= idx < len(all_books):
            return all_books[idx]
    return self.books.get(search_term)    # None if not found

① Why used

Users can find a book either by typing its title or by typing its row number. Combining both lookup strategies in one place means the rest of the code never has to worry about which input type the user chose.

② Significance

Every purchase flows through here. Making it flexible — number or title — dramatically improves usability with zero extra complexity for callers. Optional[Book] means callers must handle the None case.

③ How it operates

.isdigit() detects numeric input. The - 1 converts display numbering (starts at 1) to list indexing (starts at 0). Text input falls through to a direct dictionary lookup. Returns None if nothing matches.


04 —

The Purchase Flow

The purchase method is the most critical in the program. It orchestrates the entire buying process — three sequential safety checks, calculation, stock update, and sale recording.

Bookstore.purchase
def purchase(self, customer_name: str, title_or_idx: str, quantity: int) -> None:
    book = self.find_book(title_or_idx)

    if not book:                                 # Guard 1 — book exists?
        print(f"\n  [!] '{title_or_idx}' not found."); return

    if quantity < 1:                             # Guard 2 — qty ≥ 1?
        print("\n  [!] Quantity must be at least 1."); return

    if not book.can_sell(quantity):             # Guard 3 — enough stock?
        print(f"\n  [!] Only {book.quantity} copies left."); return

    total     = book.price_php * quantity         # calculate
    book.sell(quantity)                         # deduct stock
    timestamp = datetime.now().strftime("%Y-%m-%d %I:%M %p")
    self.sales_log.append({                    # record sale
        "time": timestamp, "customer": customer_name,
        "title": book.title, "qty": quantity, "total": total
    })
    print(f"\n  ✅ Purchase successful!")
    print(f"  Customer: {customer_name} | Total: ₱{total:,.2f}")
Guard 1 Does the book exist?
Guard 2 Is quantity ≥ 1?
Guard 3 Is stock sufficient?
All clear Calculate · Deduct · Log

① Why used

Layered validation ensures no money is calculated, no stock reduced, and no sale recorded unless every condition is fully satisfied. Any failure exits immediately with a clear error message.

② Significance

This pattern — guard clauses or early return — keeps the success path readable and flat. Deeply nested if/else becomes unreadable as conditions grow. Flat is better than nested (The Zen of Python).

③ How it operates

After all guards pass: price × quantity calculates total, book.sell() deducts stock, datetime.now().strftime(...) records the timestamp, and a dictionary with all sale details is appended to sales_log.

🕐
About the timestamp format: "%Y-%m-%d %I:%M %p" produces output like 2026-03-06 02:45 PM. %I is 12-hour clock, %p adds AM/PM. The %Y-%m-%d ordering makes records sort chronologically by default.

05 —

The main() Function

main() — the interactive menu loop
def main():
    store = Bookstore("Narra Book Haven")
    store.populate_with_sample_books()

    print("="*90)
    print(f"{store.name:^90}")   # :^90 — centre-align in 90 characters
    print("="*90)

    while True:
        print("\n[1] View All Books  [2] Buy a Book  [3] Exit")
        choice = input("Enter Choice: ").strip()

        if choice == "1":
            store.list_available_books()

        elif choice == "2":
            store.list_available_books()
            target   = input("Enter Title or Number (#): ").strip()
            try:
                qty  = int(input("How many copies? : "))
            except ValueError:
                print("  Invalid quantity.")
                continue
            customer = input("Your name: ").strip() or "Guest"
            store.purchase(customer, target, qty)

        elif choice == "3":
            total_rev = sum(s['total'] for s in store.sales_log)
            print(f"\nTotal Revenue: ₱{total_rev:,.2f}")
            break

① Why used

while True creates an infinite loop that keeps the menu running until the user explicitly chooses option 3. This is the standard pattern for any interactive terminal menu in Python.

② Significance

Without this loop, the program runs once and immediately terminates. The loop transforms it from a one-shot script into an interactive application — cleanly separating program logic (the classes) from program control (the menu).

③ How it operates

Option 1list_available_books(). Option 2 → collects input, wraps quantity in try/except to catch non-numeric typing, calls purchase(). Option 3 → generator expression sums all totals, then break exits.

🔁
The try/except ValueError block: If a user types "abc" when asked for a quantity, int("abc") raises ValueError and would crash the program. Wrapping it in try/except catches that error gracefully, prints a message, and continue restarts the loop — the program never crashes from bad typing.
🔤
The or "Guest" trick: input(...).strip() or "Guest" — if the user presses Enter without typing, .strip() returns an empty string "", which is falsy in Python. The or then falls back to "Guest". Clean one-liner default.

06 —

The Entry Point Guard

Lines 192–193
if __name__ == "__main__":
    main()

① Why used

This guards against main() running automatically when another Python file imports this module. It is a universal Python convention used in every professional script and library.

② Significance

It makes the file dual-purpose — it can run as a standalone program and be imported as a module by other files that want to reuse Book or Bookstore without launching the interactive menu.

③ How it operates

Python sets __name__ to "__main__" when the file is run directly. When imported, __name__ becomes the module filename. The if check exploits this — only a direct run triggers main().


07 —

Design Patterns in the Code

This program uses several well-known design patterns — recurring solutions to common programming problems. Recognising them helps you apply the same thinking to new projects.

🏭

Factory / Constructor Pattern

Both Book.__init__ and Bookstore.__init__ act as factories — every valid object of that type is created through them. This centralises data validation so nothing invalid ever enters the system.

🛡️

Guard Clause (Early Return)

Instead of deeply nested if/else, purchase() checks failure conditions first and returns immediately. The happy path is flat, readable, and at the bottom — easy to scan at a glance.

🔄

Upsert (Insert or Update)

add_book() checks whether a book already exists: if yes, it restocks; if no, it creates. One method handles both cases — callers don't need to know which scenario applies.

📋

Append-Only Log

sales_log is never modified after a sale is appended. Each entry is immutable history — making the log safe, auditable, and trivially summed with a generator expression.

🔗

Composition over Inheritance

Bookstore contains Book objects rather than inheriting from them. Composition keeps classes small, testable, and loosely coupled — replacing Book later requires no changes to Bookstore.

🎭

Adapter / Normaliser

Both add_book and find_book normalise input (.strip().lower()) before using it as a dictionary key. Callers don't need to know about the internal key format.

📐
The Zen of Python applies here: "Flat is better than nested." "Explicit is better than implicit." "Errors should never pass silently." Open a Python interpreter and run import this to read all 19 principles — they explain every design decision in this codebase.

08 —

Python OOP Cheat Sheet

Every syntax pattern and built-in used in this program, with a concise definition. Bookmark this section for quick reference while coding.

Class Anatomy
class Name:Define a class blueprint
def __init__(self, ...):Constructor — runs on creation
self.attr = valInstance attribute
def method(self):Instance method
def __str__(self):Called by print(obj)
def __repr__(self):Called in REPL / debug
@classmethodReceives class, not instance
@staticmethodReceives neither; utility fn
Type Hints
param: strString parameter
param: intInteger parameter
param: floatFloat parameter
-> boolReturns a boolean
-> NoneReturns nothing
Optional[T]T or None
Dict[K, V]Dict with key K, value V
list[T] (3.9+)List of T without import
String Formatting
f"{val}"Basic f-string
f"{val:<20}"Left-align, 20 wide
f"{val:>20}"Right-align, 20 wide
f"{val:^20}"Centre-align, 20 wide
f"{val:,.2f}"Comma + 2 decimal places
f"{val:08d}"Zero-pad to 8 digits
"%Y-%m-%d"strftime: 2026-03-06
"%I:%M %p"strftime: 02:45 PM
Error Handling & Control
try: / except E:Catch specific exception
except (A, B):Catch multiple exceptions
except E as e:Bind exception to name
finally:Always runs (cleanup)
raise ValueError(msg)Raise an exception
continueSkip to next loop iteration
breakExit the loop entirely
returnExit function (early return)
FeaturePythonJava equivalentNotes
Class definitionclass Book:class Book {}Python needs no braces
Constructordef __init__(self)Book() {}self is explicit in Python
Access modifiers_prot / __priv (convention)private / protectedPython is convention-based
Interface/ProtocolProtocol (3.8+) or ABCinterfacePython uses duck typing
Multiple inheritance✓ Native✗ Not allowedPython uses MRO to resolve order
Type hintsOptional, runtime-ignoredMandatoryChecked by mypy/pyright
Operator overloading✓ via dundersPartial__add__, __eq__, etc.

09 —

Practice Challenges

The best way to internalise OOP is to extend the system yourself. Each challenge below targets a specific concept covered in this guide. Try them in order — each one builds on the last.

01

Add a __repr__ method to Book

Implement __repr__ so that typing a Book object in the Python REPL shows something like Book(title="Dune", price=590.00, stock=15). This is what developers see when debugging.

Easy
02

Add a category attribute to Book

Extend Book.__init__ to accept a category: str = "Fiction" argument. Then add a Bookstore.list_by_category(cat) method that prints only books in that category.

Easy
03

Implement a print_receipt method

Add Bookstore.print_receipt() that prints all entries in sales_log as a formatted receipt table — customer name, title, quantity, price per unit, and line total. Use f-string column alignment.

Easy
04

Add a discount system using a property

Add a discount attribute to Book (default 0.0 for 0%). Add a @property called final_price that returns price_php * (1 - discount). Use final_price in the purchase calculation.

Medium
05

Make Book objects sortable by price

Implement __lt__ on Book so that sorted(bookstore.books.values()) returns books sorted by price. Then add a menu option [4] that lists books sorted cheapest-first.

Medium
06

Create an EBook subclass using inheritance

Create class EBook(Book): that adds a file_format: str (e.g. "PDF", "ePub") and a download_size_mb: float. Override __str__ to append this info. Store both physical and digital books in the same inventory.

Medium
07

Persist inventory to a JSON file

Add Bookstore.save_to_json(path) and Bookstore.load_from_json(path) using Python's built-in json module. Books should survive the program restarting — inventory persists across sessions.

Hard
08

Build a unit test suite with unittest

Create test_bookstore.py using Python's standard unittest module. Write at least 8 test cases: test that can_sell correctly handles edge cases (0, exact stock, over stock), that add_book restocks correctly, and that purchase rejects invalid inputs.

Hard
💬
Study tip: After each challenge, read your code aloud and explain every line. If you can explain it clearly, you understand it. If you can't, that's the exact gap to study. The act of teaching — even to yourself — is the most reliable path to genuine mastery.

10 —

Key Terms Glossary

Every important term used in this guide, defined precisely in the context of Python OOP.

class A blueprint for creating objects. Defines the attributes (data) and methods (behaviour) that all instances of that type will share.
instance / object A specific realisation of a class. Book("Dune", ...) creates one instance of the Book class. Each instance has its own copy of the attributes.
self The first parameter of every instance method. Python passes the object itself here automatically — it lets methods read and modify the object's own attributes.
attribute A variable that belongs to an object. self.title, self.price_php, and self.quantity are all instance attributes of Book.
method A function defined inside a class. Methods operate on self — the instance — giving them access to the object's data and other methods.
dunder / magic method Methods with double-underscore prefix and suffix (e.g. __init__, __str__). Python calls these automatically in response to built-in operations like print(), len(), or +.
encapsulation Bundling data and the methods that operate on it inside one class. Protects data integrity by controlling how attributes are accessed and modified.
type hint / annotation Optional declarations like param: str or -> bool that describe expected types. Ignored at runtime; used by editors and tools like mypy to catch bugs before they happen.
guard clause An early return (or continue) that exits a function immediately when a pre-condition fails. Keeps the success path flat and readable instead of deeply nested.
upsert A combined "Update if exists, Insert if new" operation. Used in add_book so callers don't need to know which case applies.
generator expression A lazy, memory-efficient way to iterate: sum(s['total'] for s in sales_log). Unlike a list comprehension, it computes values on demand without building the full list first.
composition When one class holds instances of another class as attributes (e.g. Bookstore containing a dict of Book objects). Usually preferred over inheritance for "has-a" relationships.
__name__ == "__main__" A Python idiom that runs code only when the file is executed directly — not when it's imported as a module by another file. Makes files dual-purpose: runnable and importable.

11 —

Complete Summary

📦

Encapsulation

Each class owns its own data. Book manages its title, price, and stock internally. Bookstore manages its inventory and sales log. Neither class reaches into the other's internals directly.

🛡️

Validation

Data is cleaned on entry (.strip(), max(0,...), round(...,2)), checked on use (can_sell, guard clauses), and never mutated without first passing a check.

🔗

Separation of Concerns

Book knows only about itself. Bookstore knows how to manage books. main() knows only how to talk to the user. Each layer does exactly one job.

What every method does

  • Book.__init__ — creates and cleans a book
  • Book.__str__ — formats a book as a table row
  • Book.can_sell — checks if stock allows a sale
  • Book.sell — deducts sold quantity from stock
  • Bookstore.__init__ — creates empty store
  • Bookstore.add_book — adds or restocks a book
  • Bookstore.find_book — searches by title or row number
  • Bookstore.purchase — validates, sells, and logs a transaction
  • main() — runs the interactive menu loop

Key Python concepts used

  • Dunder methods__init__, __str__
  • f-string formatting — alignment, decimals, padding
  • Dictionary — O(1) inventory lookup by key
  • Type hintsDict, Optional, str, int
  • Guard clauses — early return on invalid state
  • try/except — graceful input error handling
  • Generator expressionsum(s['total'] for s in ...)
  • Entry point guardif __name__ == "__main__"
  • Upsert pattern — insert-or-update in add_book

12 —

Narra Book Haven — Inside the Store

The Narra Book Haven is not just a code demo — it is a faithful model of a real Filipino independent bookstore. Every design decision in the Python code mirrors a real-world business rule. Here's what makes the store work.

📚

Curated Philippine Catalogue

The inventory opens with Rizal's Noli Me Tangere and El Filibusterismo, Balagtas's Florante at Laura, and the legendary Ibong Adarna — literature that shaped Philippine national identity. The catalogue reflects what a real independent Filipino bookstore would proudly carry.

Philippine Peso Pricing

All prices are denominated in Philippine Pesos (₱). The formatting code ₱{total:,.2f} ensures every receipt reads naturally — ₱1,250.00 rather than bare floats. The round(..., 2) constructor prevents floating-point drift on large orders.

🛡️

Inventory Protection

The can_sell → sell two-step guard is a deliberate architectural pattern: stock is never decremented without first passing a validation gate. This means the inventory can never go negative, no matter how the calling code evolves.

🔎

Flexible Search

Customers can find books by typing the title — or just the row number from the display. The find_book method handles both automatically, making the store friendly for casual browsers who don't know the exact title spelling.

📋

Sales Audit Log

Every transaction appends a timestamped dictionary to sales_log. This is a simplified version of the audit trail that every real point-of-sale system maintains — customer, title, quantity, total, and timestamp.

🧹

Silent Data Sanitisation

.strip() on names, max(0, ...) on quantity, and round(..., 2) on price are all applied silently at construction. Bad input is corrected without crashing. This is the "defensive programming" approach used in production retail systems.

🇵🇭
Why "Narra"? The Narra (Pterocarpus indicus) is the national tree of the Philippines — prized for its strength, beauty, and resilience. Naming the bookstore "Narra Book Haven" grounds the code in Filipino cultural identity and reminds us that software, like literature, can carry meaning beyond its mechanics.

The Full Inventory at Launch

#TitleAuthorPrice (₱)StockGenre
1Noli Me TangereJose Rizal220.0010Philippine Classic
2El FilibusterismoJose Rizal195.008Philippine Classic
3Florante at LauraFrancisco Balagtas150.0012Philippine Epic
4Ibong AdarnaAnonymous175.006Philippine Folk Epic
5The Great GatsbyF. Scott Fitzgerald310.005American Classic

What Happens at Each Purchase Step

Customer inputs title or row number

main() collects the user's input string and passes it directly to purchase(). No pre-processing — the lookup logic lives entirely inside the class.

find_book resolves the title

The search is normalised to lowercase and stripped. If it's numeric, it indexes the values list. If text, it hits the dictionary directly. Returns a Book object or None.

Guard clauses validate the request

Three checks run in sequence: book must exist, quantity must be ≥ 1, and stock must cover the request. Any failure prints a clear error and returns immediately — the success path never nests.

Stock is decremented and sale is logged

book.sell(qty) reduces the quantity. The transaction — time, customer, title, qty, total — is appended to sales_log with datetime.now().

Receipt is printed to the terminal

A confirmation line shows customer name and total in Philippine Peso format. At exit, the cumulative revenue across all transactions is calculated via a generator expression and displayed.

// AI-Powered

Share This Guide

Let our AI write a platform-optimised caption for you — tailored to your chosen network and ready to paste.

Select a platform and click Generate…
// Newsletter

Stay sharp.
Code better.

New Python deep-dives, OOP walkthroughs, design pattern breakdowns, and real project tutorials — delivered to your inbox every two weeks.

No spam. Unsubscribe any time.
New OOP tutorial every two weeks
Code snippets & cheat sheets included
Exclusive challenge walkthroughs
9,312 developers already subscribed

Your data stays private. We never sell or share subscriber information.

// Discussion

Join the Conversation

Got a question about the bookstore code, a pattern to suggest, or a challenge solution to share? Leave a comment below.

Leave a Comment

0 / 1000