EASYTUTORGUIDE

Practical tutorials, tools, courses, digital skills, and business promotion.

Free Learning

JavaScript – Chapter 2: Development Environment

Learn what JavaScript is, where it runs, why it is important, and how beginners can start using it with simple examples and clear output.

Beginner Friendly JavaScript Basics Web Development Code Examples
JavaScript Lesson 2 Chapter 2 Topics Day / Night Mode

Main reading content

python-course-chapter-27

Chapter 27: Decorators

A complete beginner-friendly guide to Python decorators, function wrapping, decorator arguments, metadata preservation, caching, timing, authentication, and practical applications.

Goal: Understand how decorators extend functions and classes without changing their original code, and learn how to create reusable decorators for logging, timing, caching, validation, and access control.

Chapter 27 Topics

27.1 Introduction to Decorators

```

A decorator is a Python tool that adds new behavior to a function or class without directly changing its original code. A decorator receives a function, creates a new function around it, and returns the new version. This process is often described as wrapping a function.

Decorators are useful when the same extra behavior must be applied to several functions. Common uses include logging, measuring execution time, checking permissions, validating input, caching results, counting calls, and handling errors. They reduce repeated code and keep programs organized.

Example

def simple_decorator(function):
def wrapper():
    print("Before the function runs")

    function()

    print("After the function runs")

return wrapper
```

def greet():
print("Hello!")

decorated_greet = simple_decorator(greet)

decorated_greet()
```

Output

Before the function runs
```

Hello!
After the function runs
```

Output Explanation

The decorator receives the original greet() function. It returns a wrapper that prints a message before and after calling greet(). The original function still prints Hello!, but the decorator adds extra behavior around it.

```

27.2 Functions as Objects

```

In Python, functions are objects. This means a function can be stored in a variable, passed to another function, placed inside a collection, or returned from another function. Decorators work because functions can be handled like other Python values.

Assigning a function to another variable does not call the function. The variable receives a reference to the function object. Parentheses are added only when you want to execute the function.

Example

def welcome():
print("Welcome to Python!")
```

# Store the function in another variable

message_function = welcome

# Display the function object

print(message_function)

# Call the function through the new variable

message_function()
```

Example Output

<function welcome at 0x000001AB12345670>
```

Welcome to Python!
```

Output Explanation

The first output shows that message_function refers to a function object. The memory address will differ on each computer. The second line appears when the function is called through the new variable.

```

27.3 Nested Functions

```

A nested function is a function defined inside another function. The inner function normally exists only while the outer function is running. Nested functions are useful for organizing helper logic that should not be available everywhere in the program.

Decorators usually contain a nested wrapper function. The outer decorator receives the original function, while the inner wrapper controls what happens before, during, and after the original function runs.

Example

def outer_function():
print("The outer function started")

def inner_function():
    print("The inner function is running")

inner_function()

print("The outer function finished")
```

outer_function()
```

Output

The outer function started
```

The inner function is running
The outer function finished
```

Output Explanation

The outer function begins and defines the inner function. The inner function runs only when it is called inside the outer function. After it finishes, execution returns to the outer function.

```

27.4 Closures Review

```

A closure is an inner function that remembers values from its enclosing function even after the outer function has finished. This happens because the inner function keeps access to variables that were available when it was created.

Closures are important for decorators because a wrapper must remember the original function. Even after the decorator function finishes, the wrapper still knows which original function it should call.

Example

def create_multiplier(multiplier):
def multiply(number):
    return number * multiplier

return multiply
```

double = create_multiplier(2)
triple = create_multiplier(3)

print(double(5))
print(triple(5))
```

Output

10
```

15
```

Output Explanation

The double function remembers the value 2, while the triple function remembers the value 3. The outer function has already finished, but each returned function still remembers its multiplier.

```

27.5 Creating Simple Decorators

```

A simple decorator usually contains three parts. First, the outer function receives the function being decorated. Second, a nested wrapper performs extra work and calls the original function. Third, the decorator returns the wrapper.

The result is a new function that includes both the original behavior and the additional decorator behavior. The original function name can then be replaced with the decorated version.

Example

def add_lines(function):
def wrapper():
    print("--------------------")

    function()

    print("--------------------")

return wrapper
```

def show_message():
print("Python decorators are useful.")

show_message = add_lines(show_message)

show_message()
```

Output

--------------------
```

Python decorators are useful.
--------------------
```

Output Explanation

The decorator adds a line before and after the original message. The assignment replaces show_message with the wrapper returned by add_lines().

```

27.6 The @ Syntax

```

Python provides the @ syntax as a shorter and clearer way to apply decorators. The decorator name is placed directly above the function definition. Python then automatically passes the function to the decorator and replaces it with the returned wrapper.

Writing @add_lines above a function is equivalent to manually writing function = add_lines(function) after the function definition. The decorator syntax makes the relationship easier to see.

Example

def add_lines(function):
def wrapper():
    print("====================")

    function()

    print("====================")

return wrapper
```

@add_lines
def display_title():
print("Python Course")

display_title()
```

Output

====================
```

Python Course
====================
```

Output Explanation

The @add_lines line applies the decorator to display_title(). Calling the function now runs the wrapper, which displays a line, calls the original function, and displays another line.

```

27.7 Decorating Functions

```

A decorator can modify the behavior of many different functions. The same decorator may be applied to any compatible function, allowing programs to reuse one piece of logic without copying it into every function.

In the following example, the decorator announces when a function starts and finishes. The same decorator is applied to two separate functions, showing how decorators help remove repeated code.

Example

def announce(function):
def wrapper():
    print(function.__name__, "is starting")

    function()

    print(function.__name__, "has finished")

return wrapper
```

@announce
def prepare_report():
print("Preparing the report...")

@announce
def send_email():
print("Sending the email...")

prepare_report()
print()
send_email()
```

Output

prepare_report is starting
```

Preparing the report...
prepare_report has finished

send_email is starting
Sending the email...
send_email has finished
```

Output Explanation

Both functions receive the same starting and finishing behavior. The decorator uses function.__name__ to display the original function's name.

```

27.8 Decorators with Arguments

```

A decorator can receive its own arguments. This requires an additional outer function. The first function receives the decorator settings, the second receives the function being decorated, and the third wrapper runs when the decorated function is called.

Decorator arguments make decorators more flexible. For example, one decorator could repeat a function a selected number of times, display a custom label, require a specific role, or use a chosen logging level.

Example

def repeat(times):
def decorator(function):
    def wrapper():
        for _ in range(times):
            function()

    return wrapper

return decorator
```

@repeat(3)
def say_hello():
print("Hello!")

say_hello()
```

Output

Hello!
```

Hello!
Hello!
```

Output Explanation

The value 3 is passed to repeat(). The decorator remembers this value and calls the original function three times whenever say_hello() is executed.

```

27.9 Functions with Arguments

```

A wrapper must accept the same arguments as the decorated function. Instead of listing every possible parameter, decorators commonly use *args and **kwargs. The *args parameter collects positional arguments, while **kwargs collects keyword arguments.

This approach allows one decorator to work with many functions, even when those functions have different parameters. The wrapper passes all received arguments to the original function.

Example

def show_arguments(function):
def wrapper(*args, **kwargs):
    print("Positional arguments:", args)
    print("Keyword arguments:", kwargs)

    result = function(*args, **kwargs)

    return result

return wrapper
```

@show_arguments
def introduce(name, age, city="Toronto"):
return f"{name} is {age} years old and lives in {city}."

message = introduce("Michael", 12, city="Richmond Hill")

print(message)
```

Output

Positional arguments: ('Michael', 12)
```

Keyword arguments: {'city': 'Richmond Hill'}
Michael is 12 years old and lives in Richmond Hill.
```

Output Explanation

The wrapper receives two positional arguments and one keyword argument. It displays them and then passes them to the original function. The function's return value is saved and returned by the wrapper.

```

27.10 Preserving Function Metadata

```

Functions contain metadata such as their name, documentation string, module, and annotations. A basic decorator replaces the original function with a wrapper. As a result, information such as __name__ and __doc__ may describe the wrapper instead of the original function.

Losing metadata can make debugging, documentation, and development tools less accurate. Python provides functools.wraps to copy important information from the original function to the wrapper.

Example Without Metadata Preservation

def decorator(function):
def wrapper():
    """Documentation for the wrapper."""
    return function()

return wrapper
```

@decorator
def greet():
"""Display a greeting."""
print("Hello!")

print("Function name:", greet.**name**)
print("Documentation:", greet.**doc**)
```

Output

Function name: wrapper
```

Documentation: Documentation for the wrapper.
```

Output Explanation

The decorated function now reports its name as wrapper. Its documentation also belongs to the wrapper. The original function metadata has been hidden by the decorator.

```

27.11 functools.wraps

```

The functools.wraps decorator copies metadata from an original function to its wrapper. It should normally be used whenever you create a function decorator. This keeps the decorated function's name, documentation, annotations, and other information accurate.

The @wraps(function) line is placed directly above the wrapper definition. It also creates a __wrapped__ attribute that points to the original function.

Example

from functools import wraps
```

def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
print("Calling:", function.**name**)
return function(*args, **kwargs)

```
return wrapper
```

@decorator
def greet(name):
"""Display a personalized greeting."""
return f"Hello, {name}!"

print(greet("Sara"))
print("Function name:", greet.**name**)
print("Documentation:", greet.**doc**)
```

Output

Calling: greet
```

Hello, Sara!
Function name: greet
Documentation: Display a personalized greeting.
```

Output Explanation

The decorated function keeps the name greet and its original documentation string. The decorator still adds behavior, but the function's identity remains accurate.

```

27.12 Multiple Decorators

```

A function can have more than one decorator. Each decorator adds its own behavior. Multiple decorators are written on separate lines above the function definition.

The decorator closest to the function is applied first. The decorator above it then wraps the result. This is similar to placing one wrapped package inside another wrapped package.

Example

from functools import wraps
```

def uppercase(function):
@wraps(function)
def wrapper(*args, **kwargs):
result = function(*args, **kwargs)
return result.upper()

```
return wrapper
```

def add_stars(function):
@wraps(function)
def wrapper(*args, **kwargs):
result = function(*args, **kwargs)
return "*** " + result + " ***"

```
return wrapper
```

@add_stars
@uppercase
def greeting(name):
return f"Hello, {name}"

print(greeting("Michael"))
```

Output

*** HELLO, MICHAEL ***

Output Explanation

The uppercase decorator is closest to the function, so it converts the greeting to uppercase first. The add_stars decorator then adds stars around the uppercase result.

```

27.13 Decorator Order

```

Decorator order affects the final result. When several decorators are stacked, Python applies them from bottom to top. However, when the decorated function runs, the outermost wrapper begins first.

Understanding order is important when decorators transform return values, check permissions, open resources, or display messages. Reversing the order can produce a different result.

Example

from functools import wraps
```

def first(function):
@wraps(function)
def wrapper():
print("First decorator: before")

```
    function()

    print("First decorator: after")

return wrapper
```

def second(function):
@wraps(function)
def wrapper():
print("Second decorator: before")

```
    function()

    print("Second decorator: after")

return wrapper
```

@first
@second
def task():
print("Original task")

task()
```

Output

First decorator: before
```

Second decorator: before
Original task
Second decorator: after
First decorator: after
```

Output Explanation

The second decorator is applied first because it is closest to the function. The first decorator wraps the result. During execution, the outer first wrapper starts before the inner second wrapper.

```

27.14 Class Decorators

```

A decorator can be implemented as a class instead of a function. The class receives the original function in its constructor. It then defines a __call__() method so instances of the class can behave like callable functions.

Class decorators are useful when the decorator needs to store state, such as the number of times a function was called. The stored information can remain available between calls.

Example

class CallCounter:
def __init__(self, function):
    self.function = function
    self.count = 0

def __call__(self, *args, **kwargs):
    self.count += 1

    print("Call number:", self.count)

    return self.function(*args, **kwargs)
```

@CallCounter
def greet(name):
print("Hello,", name)

greet("Ali")
greet("Sara")
greet("Michael")
```

Output

Call number: 1
```

Hello, Ali
Call number: 2
Hello, Sara
Call number: 3
Hello, Michael
```

Output Explanation

The class stores the original function and a call counter. Each time the decorated function runs, __call__() increases the counter before calling the original function.

```

27.15 Decorating Methods

```

Methods inside classes can also be decorated. The wrapper must accept self along with any other arguments. Using *args and **kwargs automatically includes the instance argument.

Method decorators can log object actions, validate method arguments, check permissions, measure performance, or prevent methods from running under certain conditions.

Example

from functools import wraps
```

def log_method(function):
@wraps(function)
def wrapper(*args, **kwargs):
print("Running method:", function.**name**)

```
    result = function(*args, **kwargs)

    print("Method finished:", function.__name__)

    return result

return wrapper
```

class BankAccount:
def **init**(self, balance):
self.balance = balance

```
@log_method
def deposit(self, amount):
    self.balance += amount
    return self.balance
```

account = BankAccount(500)

print("New balance:", account.deposit(200))
```

Output

Running method: deposit
```

Method finished: deposit
New balance: 700
```

Output Explanation

The decorator logs the method name before and after the deposit. The original method updates the account balance and returns the new value.

```

27.16 Caching Decorators

```

Caching stores the result of a function call so the program can reuse it when the same inputs appear again. This can improve performance when a function performs an expensive calculation or repeatedly requests the same data.

Python provides functools.cache and functools.lru_cache. The lru_cache decorator can limit how many results are stored. Cached function arguments generally need to be hashable values such as numbers, strings, and tuples.

Example

from functools import lru_cache
```

@lru_cache(maxsize=4)
def calculate_square(number):
print("Calculating square of", number)
return number ** 2

print(calculate_square(5))
print(calculate_square(5))
print(calculate_square(10))
print(calculate_square(10))
```

Output

Calculating square of 5
```

25
25
Calculating square of 10
100
100
```

Output Explanation

The first call for each number performs the calculation and stores the result. The second call with the same number returns the cached result, so the calculation message does not appear again.

```

27.17 Timing Decorators

```

A timing decorator measures how long a function takes to run. It records the time before calling the function, records the time afterward, and calculates the difference.

Timing decorators are useful for comparing algorithms, locating slow code, testing performance, and monitoring application operations. The time.perf_counter() function provides a precise timer for this purpose.

Example

import time
```

from functools import wraps

def measure_time(function):
@wraps(function)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()

```
    result = function(*args, **kwargs)

    end_time = time.perf_counter()
    duration = end_time - start_time

    print(function.__name__, "took", duration, "seconds")

    return result

return wrapper
```

@measure_time
def calculate_total():
total = 0

```
for number in range(1, 1000001):
    total += number

return total
```

print("Total:", calculate_total())
```

Example Output

calculate_total took 0.0524812000003 seconds
```

Total: 500000500000
```

Output Explanation

The decorator measures the duration of the calculation and prints it. The exact execution time will differ depending on the computer and current system activity. The original return value is preserved.

```

27.18 Authentication Decorators

```

An authentication decorator checks whether a user is allowed to run a function. It may verify that a user is logged in, has a required role, or owns a specific permission.

Web frameworks frequently use decorators for access control. A decorator can stop a protected function from running and return an error message when the user does not meet the requirements.

Example

from functools import wraps
```

current_user = {
"name": "Sara",
"logged_in": True,
"role": "admin"
}

def require_admin(function):
@wraps(function)
def wrapper(*args, **kwargs):
if not current_user["logged_in"]:
return "Access denied: Please log in."

```
    if current_user["role"] != "admin":
        return "Access denied: Administrator role required."

    return function(*args, **kwargs)

return wrapper
```

@require_admin
def delete_record(record_id):
return f"Record {record_id} was deleted."

print(delete_record(105))
```

Output

Record 105 was deleted.

Output Explanation

The decorator first checks whether the user is logged in and whether the role is admin. Because both conditions are true, the protected function runs and deletes the selected record.

```

27.19 Practical Decorator Applications

```

Decorators can solve many real programming problems. They are commonly used for logging, validation, retries, caching, timing, access control, formatting, counting calls, handling exceptions, and checking application state.

In this example, a validation decorator checks that a product price is positive. The decorator prevents the original function from running when the value is invalid.

Example: Price Validation Decorator

from functools import wraps
```

def require_positive_price(function):
@wraps(function)
def wrapper(product_name, price):
if not isinstance(price, (int, float)):
return "Error: Price must be a number."

```
    if price <= 0:
        return "Error: Price must be greater than zero."

    return function(product_name, price)

return wrapper
```

@require_positive_price
def create_product(product_name, price):
return f"{product_name} was created with a price of ${price:.2f}."

print(create_product("Keyboard", 49.99))
print(create_product("Monitor", -100))
print(create_product("Mouse", "twenty"))
```

Output

Keyboard was created with a price of $49.99.
```

Error: Price must be greater than zero.
Error: Price must be a number.
```

Output Explanation

The valid keyboard price passes the checks and reaches the original function. The negative monitor price and text mouse price are rejected by the decorator before the product function runs.

```

27.20 Chapter Practice Exercises

```

These exercises reinforce function objects, nested functions, closures, decorators, decorator arguments, metadata preservation, multiple decorators, class decorators, caching, timing, authentication, and validation.

  1. Store a function in a variable and call it through the variable.
  2. Pass a function as an argument to another function.
  3. Create an outer function containing a nested function.
  4. Create a closure that remembers a greeting word.
  5. Create a decorator that prints a message before a function runs.
  6. Create a decorator that prints messages before and after a function.
  7. Apply a decorator manually without using @.
  8. Apply the same decorator using @ syntax.
  9. Create a decorator that converts a returned string to uppercase.
  10. Create a decorator that repeats a function five times.
  11. Create a decorator that receives a custom title.
  12. Create a wrapper using *args and **kwargs.
  13. Create a decorator that logs function arguments.
  14. Preserve metadata using functools.wraps.
  15. Apply two decorators to one function.
  16. Reverse the decorator order and compare the output.
  17. Create a class decorator that counts function calls.
  18. Decorate a class method.
  19. Cache a calculation using lru_cache.
  20. Create a timing decorator.
  21. Create a login-check decorator.
  22. Create a role-check decorator.
  23. Create a decorator that catches exceptions.
  24. Create a decorator that validates positive numbers.
  25. Create a decorator that retries a function three times.

Practice Example: Return Value Logger

from functools import wraps
```

def log_result(function):
@wraps(function)
def wrapper(*args, **kwargs):
result = function(*args, **kwargs)

```
    print("Function:", function.__name__)
    print("Returned:", result)

    return result

return wrapper
```

@log_result
def multiply(first_number, second_number):
return first_number * second_number

answer = multiply(6, 7)

print("Answer:", answer)
```

Output

Function: multiply
```

Returned: 42
Answer: 42
```

Output Explanation

The decorator calls the original function and stores its result. It displays the function name and returned value before returning the result to the calling code.

```

27.21 Chapter Mini Project

```

Project: Decorator-Based Banking Security System

In this mini project, you will create a small banking system that uses several decorators. One decorator checks whether the user is logged in. Another checks whether the user has permission to perform administrative actions. A third logs function calls, and a fourth validates transaction amounts.

This project combines decorator arguments, *args, **kwargs, functools.wraps, methods, validation, authentication, authorization, logging, and return values.

Complete Program

from functools import wraps
```

from datetime import datetime

current_user = {
"username": "sara",
"logged_in": True,
"role": "admin"
}

activity_log = []

def log_activity(function):
"""Record when a decorated function is called."""

```
@wraps(function)
def wrapper(*args, **kwargs):
    current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    log_entry = (
        f"{current_time} | "
        f"User: {current_user['username']} | "
        f"Action: {function.__name__}"
    )

    activity_log.append(log_entry)

    print("LOG:", log_entry)

    return function(*args, **kwargs)

return wrapper
```

def require_login(function):
"""Allow the function to run only for logged-in users."""

```
@wraps(function)
def wrapper(*args, **kwargs):
    if not current_user["logged_in"]:
        return "Access denied: You must log in."

    return function(*args, **kwargs)

return wrapper
```

def require_role(required_role):
"""Allow the function to run only for a selected role."""

```
def decorator(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        if current_user["role"] != required_role:
            return (
                "Access denied: "
                f"The {required_role} role is required."
            )

        return function(*args, **kwargs)

    return wrapper

return decorator
```

def validate_amount(function):
"""Check that a transaction amount is a positive number."""

```
@wraps(function)
def wrapper(self, amount):
    if not isinstance(amount, (int, float)):
        return "Transaction failed: Amount must be a number."

    if amount <= 0:
        return "Transaction failed: Amount must be greater than zero."

    return function(self, amount)

return wrapper
```

class BankAccount:
def **init**(self, owner, balance=0):
self.owner = owner
self.balance = balance

```
@require_login
@log_activity
def view_balance(self):
    return f"{self.owner}'s balance is ${self.balance:.2f}."

@require_login
@validate_amount
@log_activity
def deposit(self, amount):
    self.balance += amount

    return (
        f"Deposit successful. "
        f"New balance: ${self.balance:.2f}."
    )

@require_login
@validate_amount
@log_activity
def withdraw(self, amount):
    if amount > self.balance:
        return "Transaction failed: Insufficient funds."

    self.balance -= amount

    return (
        f"Withdrawal successful. "
        f"New balance: ${self.balance:.2f}."
    )

@require_login
@require_role("admin")
@log_activity
def reset_balance(self):
    self.balance = 0

    return "The account balance was reset to $0.00."
```

account = BankAccount("Sara", 1000)

print("BANK ACCOUNT SYSTEM")
print("----------------------------------------")

print(account.view_balance())
print()

print(account.deposit(250))
print()

print(account.withdraw(400))
print()

print(account.withdraw(2000))
print()

print(account.deposit(-50))
print()

print(account.reset_balance())
print()

print(account.view_balance())

print()
print("ACTIVITY LOG")
print("----------------------------------------")

for entry in activity_log:
print(entry)
```

Example Output

BANK ACCOUNT SYSTEM
```

---

LOG: 2026-07-19 12:30:00 | User: sara | Action: view_balance
Sara's balance is $1000.00.

LOG: 2026-07-19 12:30:00 | User: sara | Action: deposit
Deposit successful. New balance: $1250.00.

LOG: 2026-07-19 12:30:00 | User: sara | Action: withdraw
Withdrawal successful. New balance: $850.00.

LOG: 2026-07-19 12:30:00 | User: sara | Action: withdraw
Transaction failed: Insufficient funds.

Transaction failed: Amount must be greater than zero.

LOG: 2026-07-19 12:30:00 | User: sara | Action: reset_balance
The account balance was reset to $0.00.

LOG: 2026-07-19 12:30:00 | User: sara | Action: view_balance
Sara's balance is $0.00.

## ACTIVITY LOG

2026-07-19 12:30:00 | User: sara | Action: view_balance
2026-07-19 12:30:00 | User: sara | Action: deposit
2026-07-19 12:30:00 | User: sara | Action: withdraw
2026-07-19 12:30:00 | User: sara | Action: withdraw
2026-07-19 12:30:00 | User: sara | Action: reset_balance
2026-07-19 12:30:00 | User: sara | Action: view_balance
```

Project Explanation

The log_activity decorator records the date, time, username, and function name. It stores each entry in the activity_log list and displays it when the function runs.

The require_login decorator checks the logged_in value before allowing protected methods to run. When the user is not logged in, the decorator returns an access-denied message.

The require_role() decorator receives the required role as an argument. It creates and returns another decorator. The reset_balance() method requires the admin role.

The validate_amount decorator checks that deposit and withdrawal amounts are numeric and greater than zero. Invalid amounts are rejected before the original transaction method runs.

Decorator order controls which checks happen first. For example, the login decorator is outside the validation and logging decorators. This prevents unauthorized users from reaching the protected transaction logic.

How to Run the Mini Project

  1. Open Visual Studio Code, IDLE, PyCharm, or another Python editor.
  2. Create a new file named decorator_banking_system.py.
  3. Copy the complete project code into the file.
  4. Save the file.
  5. Open a terminal in the folder containing the file.
  6. Run python decorator_banking_system.py.
  7. On some computers, run python3 decorator_banking_system.py.
  8. Review the transaction results.
  9. Review the activity log displayed at the end.
  10. Change logged_in to False and run the program again.
  11. Change the role from admin to customer and test the reset operation.

Project Challenges

  • Add a decorator that limits the number of withdrawal attempts.
  • Add a decorator that requires a PIN number.
  • Add a decorator that measures transaction execution time.
  • Add a decorator that catches unexpected errors.
  • Save the activity log to a text file.
  • Add transfer functionality between two accounts.
  • Require the admin role to view every user's activity log.
  • Add a decorator that blocks transactions above a selected amount.
  • Add daily withdrawal limits.
  • Add a class decorator that counts all account operations.
  • Cache selected account reports.
  • Create separate roles for customers, employees, and administrators.
```
Chapter 2: Development EnvironmentPrepare Your JavaScript Workspace

A modern course built to help learners study step by step with clarity, comfort, and confidence.