08: Logging, CLIs

image.png Մանկական երկաթուղի, լուսանկարի հղումը, Հեղինակ՝ Tigran Kharatyan

Open In Colab (ToDo)

“Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.” - Brian Kernighan

📌 Նկարագիր

📚 Ամբողջական նյութը

📺 Տեսագրությունը

📌 Նկարագիր Մեր առաջին սովորած ֆունկցիան՝ print-ը։ Շատ լավ օրեր ենք անցկացրել իրա հետ, բայց հիմա ժամանակն ա անցնել իրա պռոֆեսինոլ տարբերակին՝ logging-ին։ Սովորում ենք ՝ 1. ոնց նշել logging-ի մակարդակը 2. մեր ուզած ձևով ֆոռմատավորել log-երը 3. գրել log-երը ֆայլի մեջ, ինչպես նաև կոնսոլում 4. json ֆորմատով պահել լոգերը 5. exception-ներ log անել

Հետո անցնում ենք Command Line Interface(CLI)-եր սարքելու գրադարաններին։ CLI-ների շնորհիվ ա որ կարանք գրենք ուղղակի pip install panir ու ինքը գնա ավտոմատ քաշի բերի մեր ուզած գրադարանը, ոչ թե մենք բացենք կոդը որտեղ install հրամանը օգտագործվում ա ու կոդի մեջ նշենք որ արգումենտ որպես մեր գրադարանի անունը գնա։ Սովորում ենք՝ 1. Argparse-ը (ներկառուցված գրադարան) 2. Fire (ֆունկցիաներին CLI-ով դիմելու համար 1-2 տողանոց լուծում) 3. Click (դեկորատորներով աշախտող ուժեղ գրադարն) 4. Typer (FastAPI-ենց գրադարանը type hint-երի վրա հիմնված)

📚 Նյութը

Logging

Logging libraries

Docs: 1. Doc 2. Logging Cookbook

Videos: 1. Tech With Tim (15 minute video) 2. Corey Schafer, part 1 (14 minute video) 3. Corey Schafer, part 2 (20 minute video)

Ինչի՞ logging

  1. print-ով մի անգամ աշխատացրեցինք, console ում տպեց ու վերջ, հաջողություն, իսկ logging-ով կարանք հարմար save անենք
  2. պետք չի comment out անել/ջնջել, հետո հետ բերել print-երը, եթե էլ չենք ուզում մի բան տպենք, կարանք սահմամենք որ դեպքում ինչը տպվի
  3. պետք չի ձեռով գրենք երբ ենք տպել ինչ-որ բան կամ որ ֆայլից ա տպվել

First logger + levels

import logging

logging.basicConfig( # Camel case
    level=logging.DEBUG,
)

logger = logging.getLogger(__name__)

# Different logging levels
logger.debug("This is a debug message") # 10
logger.info("Application started")      # 20
logger.warning("Warning: Potential issue detected") # 30
logger.error("An error occurred")  # 40
logger.critical("Critical error!") # 50
ERROR:__main__:An error occurred
CRITICAL:__main__:Critical error!
import logging

print(logging.INFO)

pi = 3.14
20
import logging

logging.basicConfig( # Camel case
    level=24,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)

# Different logging levels
logger.debug("This is a debug message") # 10
logger.info("Application started") # 20
logger.warning("Warning: Potential issue detected") # 30
logger.error("An error occurred") # 40
logger.critical("Critical error!") # 50
2026-02-18 16:48:02,059 - __main__ - WARNING - Warning: Potential issue detected
2026-02-18 16:48:02,059 - __main__ - ERROR - An error occurred
2026-02-18 16:48:02,060 - __main__ - CRITICAL - Critical error!

Formatting

https://docs.python.org/3/library/logging.html#logrecord-attributes

import logging

logging.basicConfig( # Camel case
    level=24,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

logger = logging.getLogger(__name__)

# Different logging levels
logger.debug("This is a debug message") # 10
logger.info("Application started") # 20
logger.warning("Warning: Potential issue detected") # 30
logger.error("An error occurred") # 40
logger.critical("Critical error!") # 50
2026-02-18 16:50:18 - __main__ - WARNING - Warning: Potential issue detected
2026-02-18 16:50:18 - __main__ - ERROR - An error occurred
2026-02-18 16:50:18 - __main__ - CRITICAL - Critical error!

Logging to a file

import logging

logger = logging.getLogger('file_console_logger')
logger.setLevel(logging.DEBUG) # another way to set the level

# Create file handler
file_handler = logging.FileHandler('app.log', mode="a") # a is default
file_handler.setLevel(logging.INFO)

# Create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)

# Add handlers to logger
logger.addHandler(file_handler)
def divide_numbers(a, b):
    """Division with comprehensive logging"""
    logger.info(f"Starting division: {a} ÷ {b}")
    
    try:
        result = a / b
        logger.info(f"Division successful. Result: {result}")
        return result
    except ZeroDivisionError:
        logger.error("Division by zero error!")
        return None
    except Exception as e:
        logger.critical(f"Unexpected error: {e}")
        return None

# Test the function
result1 = divide_numbers(10, 3)
result2 = divide_numbers(509, "1")

# Show that logs go to both console and file
print(f"\nResults: {result1}, {result2}")
print("Check 'app.log' file for logged messages")
2026-02-18 16:56:49,941 - file_console_logger - INFO - Starting division: 10 ÷ 3
2026-02-18 16:56:49,943 - file_console_logger - INFO - Division successful. Result: 3.3333333333333335
2026-02-18 16:56:49,944 - file_console_logger - INFO - Starting division: 509 ÷ 1
2026-02-18 16:56:49,946 - file_console_logger - CRITICAL - Unexpected error: unsupported operand type(s) for /: 'int' and 'str'

Results: 3.3333333333333335, None
Check 'app.log' file for logged messages

Logging to console as well

import logging

logger = logging.getLogger('file_console_logger')
logger.setLevel(logging.DEBUG) # another way to set the level

# Create file handler
file_handler = logging.FileHandler('app.log', mode="a") # a is default
file_handler.setLevel(logging.INFO)

# Create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)

# Add handlers to logger
logger.addHandler(file_handler)

Console handler

# add console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter) # Use the same formatter
# Add handlers to logger
logger.addHandler(console_handler)
# Test the function
result1 = divide_numbers(10, 2)
result2 = divide_numbers(10, 0)

# Show that logs go to both console and file
print(f"\nResults: {result1}, {result2}")
print("Check 'app.log' file for logged messages")
2026-02-18 16:59:24,270 - file_console_logger - INFO - Starting division: 10 ÷ 2
2026-02-18 16:59:24,270 - file_console_logger - INFO - Starting division: 10 ÷ 2
2026-02-18 16:59:24,274 - file_console_logger - INFO - Division successful. Result: 5.0
2026-02-18 16:59:24,274 - file_console_logger - INFO - Division successful. Result: 5.0
2026-02-18 16:59:24,278 - file_console_logger - INFO - Starting division: 10 ÷ 0
2026-02-18 16:59:24,278 - file_console_logger - INFO - Starting division: 10 ÷ 0
2026-02-18 16:59:24,284 - file_console_logger - ERROR - Division by zero error!
2026-02-18 16:59:24,284 - file_console_logger - ERROR - Division by zero error!

Results: 5.0, None
Check 'app.log' file for logged messages

Json logging + Rotation file

import json
import logging
from datetime import datetime
from logging.handlers import RotatingFileHandler

# Create advanced logger
advanced_logger = logging.getLogger('advanced_app')
advanced_logger.setLevel(logging.DEBUG)

# Rotating file handler
rotating_handler = RotatingFileHandler(
    'advanced_app.log', 
    maxBytes=1024 / 2,  # 1/2 KB
    backupCount=5
)
rotating_handler.setLevel(logging.INFO)
class JSONFormatter(logging.Formatter):
    def format(self, record):
        log_entry = {
            'timestamp': datetime.fromtimestamp(record.created).isoformat(),
            'level': record.levelname,
            'message': record.getMessage(),
            'module': record.module,
            'function': record.funcName,
            'line': record.lineno
        }
        return json.dumps(log_entry, ensure_ascii=False)

json_formatter = JSONFormatter()

rotating_handler.setFormatter(json_formatter)

advanced_logger.addHandler(rotating_handler)

# Example class with logging
class Calculator:
    def __init__(self):
        self.logger = logging.getLogger(f'{__name__}.Calculator')
        self.logger.info("Calculator instance created")
    
    def add(self, a, b):
        self.logger.debug(f"Adding: {a} + {b}")
        result = a + b
        self.logger.info(f"Addition result: {result}")
        return result
    
    def multiply(self, a, b):
        self.logger.debug(f"Multiplying: {a} × {b}")
        result = a * b
        self.logger.info(f"Multiplication result: {result}")
        return result
    
    def divide(self, a, b):
        self.logger.debug(f"Dividing: {a} ÷ {b}")
        try:
            result = a / b
            self.logger.info(f"Division result: {result}")
            return result
        except ZeroDivisionError:
            self.logger.error("Division by zero attempted")
            return None

for _ in range(509):
    # Test the Calculator with logging
    calc = Calculator()
    calc.add(5, 3)
    calc.multiply(4, 7)
    calc.divide(10, 2)
    calc.divide(10, 0)  # This will generate an error log

    advanced_logger.info("Calculator operations completed")

python-json-logger

!uv pip install python-json-logger
Using Python 3.10.18 environment at: C:\Users\hayk_\.conda\envs\lectures

Audited 1 package in 21ms
import sys
import logging
from pythonjsonlogger import jsonlogger   

logger = logging.getLogger(__name__)
stdout_handler = logging.StreamHandler(stream=sys.stdout)

# format with JSON
format_output = jsonlogger.JsonFormatter('%(levelname)s : %(name)s : %(message)s : %(asctime)s')

stdout_handler.setFormatter(format_output)
logger.addHandler(stdout_handler)

logger.error("This is an error message")
logger.critical("This is a critical message")
{"levelname": "ERROR", "name": "__main__", "message": "This is an error message", "asctime": "2025-07-20 11:00:44,850"}
{"levelname": "CRITICAL", "name": "__main__", "message": "This is a critical message", "asctime": "2025-07-20 11:00:44,851"}
import sys
import logging
from pythonjsonlogger import jsonlogger

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

stdout_handler = logging.StreamHandler(stream=sys.stdout)
## Create a file handler
fileHandler = logging.FileHandler("app_with_json.log")   # <-

format_output = jsonlogger.JsonFormatter('%(levelname)s : %(name)s : %(message)s : %(asctime)s')

stdout_handler.setFormatter(format_output)

fileHandler.setFormatter(format_output)     # <-

logger.addHandler(stdout_handler)
## the file handle handler
logger.addHandler(fileHandler)             # <-

logger.error("This is an error message")
logger.critical("This is a critical message")
{"levelname": "ERROR", "name": "__main__", "message": "This is an error message", "asctime": "2025-07-20 11:01:06,254"}
{"levelname": "CRITICAL", "name": "__main__", "message": "This is a critical message", "asctime": "2025-07-20 11:01:06,256"}

Exception logging

import logging
try:
    print(509/0)
except Exception as e:
    logging.exception(e)
ERROR:root:division by zero
Traceback (most recent call last):
  File "C:\Users\hayk_\AppData\Local\Temp\ipykernel_14724\1569123368.py", line 3, in <module>
    print(509/0)
ZeroDivisionError: division by zero

Command Line Interfaces (CLIs)

Կոդերը՝ 1. Argparse 2. Fire 3. Click 4. Typer

Հղումներ՝ - Argparse docs - Argparse tutorial - Fire GitHub - Typer GitHub - Click docs

Library One‑line pitch Typical use cases Distinguishing features
Click Decorator‑driven “Command‑Line Interface Creation Kit” Production‑grade tools, multi‑command suites Rich nesting, colors, automatic Bash/Z‑sh completion, context objects; battle‑tested since 2014. (click.palletsprojects.com)
Typer Click‑powered, but driven by type hints Modern Python ≥ 3.8 projects that already lean on typing Generates help & shell completion from function signatures; async‑friendly; FastAPI‑style DX. (PyPI)
Python Fire Zero‑boilerplate – expose any Python object with fire.Fire() Throw‑away scripts, prototyping, data‑science notebooks Introspects classes, dicts, even lambdas; perfect for “I just need a CLI around this function”. (GitHub)

How to choose

  1. Small script, no dependencies? Stay on argparse.
  2. You want nicer syntax, colors, nested commands? Click is the mainstream choice.
  3. Already writing async / FastAPI‑style code with type hints? Typer feels natural.
  4. You have a pre‑written module and need a CLI now. Drop in Python Fire.

🎲 00 (ToDo)

Flag Counter