enum, copy, pprint, logging and Other Essentials

Five more modules that appear in almost every real program: named constants, safe copying, readable output, proper logging, and identifiers and hashes.

enum: named constants

from enum import Enum


class Status(Enum):
    DRAFT = "draft"
    PUBLISHED = "published"
    ARCHIVED = "archived"


print(Status.DRAFT)              # Status.DRAFT
print(Status.DRAFT.name)         # DRAFT
print(Status.DRAFT.value)        # draft
print(Status("draft"))           # Status.DRAFT - look up by value
print(Status["DRAFT"])           # Status.DRAFT - look up by name
print(list(Status))              # every member, in definition order
# Without an enum: any string is accepted, including a typo
def set_status(note, status):
    note["status"] = status

set_status({}, "publised")        # silently wrong


# With an enum: only the defined members exist
def set_status(note, status: Status):
    note["status"] = status.value

# set_status({}, Status.PUBLISED)     # AttributeError, immediately

That is the whole argument for enums: a misspelling becomes an error at the point you write it, rather than a wrong value discovered weeks later.

from enum import Enum, auto, IntEnum, Flag


class Colour(Enum):
    RED = auto()             # 1, 2, 3 assigned automatically
    GREEN = auto()
    BLUE = auto()


class Priority(IntEnum):     # behaves as an int as well
    LOW = 1
    MEDIUM = 5
    HIGH = 10


print(Priority.HIGH > Priority.LOW)      # True
print(Priority.HIGH + 1)                 # 11
print(sorted(Priority, reverse=True))


class Permission(Flag):      # combinable with bitwise operators
    READ = 1
    WRITE = 2
    EXECUTE = 4


access = Permission.READ | Permission.WRITE
print(access)                            # Permission.READ|WRITE
print(Permission.READ in access)         # True
print(Permission.EXECUTE in access)      # False
from enum import Enum


class Status(Enum):
    DRAFT = "draft"
    PUBLISHED = "published"

    @property
    def is_visible(self):
        return self is Status.PUBLISHED

    def __str__(self):
        return self.value.title()


print(Status.PUBLISHED.is_visible)       # True
print(str(Status.DRAFT))                 # Draft

copy: shallow and deep

import copy

original = {"name": "report", "tags": ["draft", "q3"], "meta": {"pages": 12}}

alias = original                        # not a copy at all
shallow = copy.copy(original)           # or original.copy()
deep = copy.deepcopy(original)

original["tags"].append("urgent")
original["meta"]["pages"] = 20

print(alias["tags"])       # ['draft', 'q3', 'urgent']
print(shallow["tags"])     # ['draft', 'q3', 'urgent']  <- inner list shared
print(deep["tags"])        # ['draft', 'q3']             <- fully independent
shallow copy                    deep copy

outer dict  ──► new dict        outer dict  ──► new dict
   │              │                 │              │
   └──► [list] ◄──┘                 └──► [list]    └──► [new list]
        (one shared object)              (two separate objects)
UseWhen
AssignmentYou want another name for the same object
copy.copyEvery value inside is immutable
copy.deepcopyThere are nested mutable values you will change

deepcopy is slow and it copies everything, including objects you may not want duplicated. Reach for it when you need it and not by default.

pprint: readable output

from pprint import pprint, pformat

data = {
    "name": "Lumen Works",
    "departments": [
        {"name": "engineering", "staff": ["Meera", "Arun", "Sara"]},
        {"name": "design", "staff": ["Ravi"]},
    ],
    "founded": 2019,
}

print(data)          # one long unreadable line
print("---")
pprint(data)         # indented and wrapped
pprint(data, width=40, depth=2)
pprint(data, sort_dicts=False)     # keep insertion order

text = pformat(data)               # the same, returned as a string
print(len(text))

For debugging nested structures, pprint is the difference between reading the output and squinting at it.

logging: better than print

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s  %(levelname)-8s  %(name)s  %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

logger = logging.getLogger(__name__)

logger.debug("detailed state, hidden at INFO level")
logger.info("service started")
logger.warning("disk usage at 85 percent")
logger.error("could not reach the database")
logger.critical("shutting down")
2026-08-22 14:30:00  INFO      __main__  service started
2026-08-22 14:30:00  WARNING   __main__  disk usage at 85 percent
2026-08-22 14:30:00  ERROR     __main__  could not reach the database
2026-08-22 14:30:00  CRITICAL  __main__  shutting down
LevelValueUse for
DEBUG10Detail useful only while diagnosing
INFO20Normal progress worth recording
WARNING30Something unexpected, still working
ERROR40An operation failed
CRITICAL50The program cannot continue

Why not print

  • Levels: turn detail on or off without editing code.
  • Destinations: console, file, or both, decided by configuration.
  • Context: timestamp, module name and line number, automatically.
  • Exceptions: logger.exception records the full traceback.
import logging

logger = logging.getLogger(__name__)

try:
    1 / 0
except ZeroDivisionError:
    logger.exception("calculation failed")     # message plus full traceback


# Pass arguments separately - formatting is skipped if the level is disabled
logger.info("user %s completed %d tasks", "meera", 5)
# not: logger.info(f"user {name} completed {count} tasks")
import logging

logger = logging.getLogger("notesapp")
logger.setLevel(logging.DEBUG)

console = logging.StreamHandler()
console.setLevel(logging.WARNING)                # only warnings on screen

file_handler = logging.FileHandler("app.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)             # everything to the file

formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
console.setFormatter(formatter)
file_handler.setFormatter(formatter)

logger.addHandler(console)
logger.addHandler(file_handler)

logger.debug("only in the file")
logger.warning("in both")
Call logging.getLogger(__name__) in each module and configure logging once, in the program's entry point. A library should never call basicConfig; that decision belongs to the application using it.

uuid, hashlib and textwrap

import uuid

print(uuid.uuid4())              # a random unique identifier
print(uuid.uuid4().hex)          # without the dashes
print(str(uuid.uuid4())[:8])     # a short id, when collisions are tolerable
import hashlib

data = b"the quick brown fox"

print(hashlib.sha256(data).hexdigest())
print(hashlib.md5(data).hexdigest())         # for checksums only, never security


def file_checksum(path, algorithm="sha256"):
    digest = hashlib.new(algorithm)
    with open(path, "rb") as handle:
        while chunk := handle.read(65536):
            digest.update(chunk)
    return digest.hexdigest()

Hashing is not encryption; it is one way. For storing passwords, a plain SHA-256 is not enough - a deliberately slow algorithm is required. The security note covers this.

import textwrap

long_text = "Python is a high level language whose design emphasises readability above almost everything else."

print(textwrap.fill(long_text, width=40))
print()
print(textwrap.indent(textwrap.fill(long_text, 40), "> "))
print()
print(textwrap.shorten(long_text, width=50, placeholder=" ..."))

code = """
    def f():
        return 1
"""
print(textwrap.dedent(code))     # strips the common leading whitespace

Common mistakes

  • Comparing an enum member to its raw value: Status.DRAFT == "draft" is False for a plain Enum.
  • Using copy.copy on nested data and being surprised by shared inner objects.
  • Using print for diagnostics in anything long lived.
  • Formatting a log message with an f-string, so the work happens even when the level is off.
  • Calling basicConfig from inside a library.
  • Using md5 or a bare sha256 for passwords.

Best practices

  • Use an Enum for any fixed set of named options, and IntEnum when ordering matters.
  • Use deepcopy only when nested mutable data will be modified.
  • Use pprint while debugging nested structures.
  • Use logging with getLogger(__name__) in every module, configured once at the entry point.
  • Pass log arguments separately rather than pre-formatting.
  • Use uuid4 for identifiers and secrets for anything that must be unguessable.

Practice

  1. Define an enum for order states and write a function that only accepts its members.
  2. Demonstrate the difference between copy and deepcopy on a three level structure.
  3. Configure logging so that DEBUG goes to a file and WARNING goes to the console.
  4. Write a checksum function and prove two identical files produce the same digest.
  5. Use textwrap to format a paragraph into a 60 character quoted block.

Conclusion

enum replaces loose strings with checked names, copy decides how much of a structure you duplicate, pprint makes nested data readable, and logging replaces print everywhere it matters. All four appear in more or less every real Python program.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.