Usage Guide

Basic Usage

The simplest way to use pycodify is to generate source code for an object:

from pycodify import Assignment, generate_python_source
from dataclasses import dataclass

@dataclass
class ProcessingConfig:
    input_path: str = "/data/input"
    output_path: str = "/data/output"
    num_workers: int = 4

config = ProcessingConfig(
    input_path="/data/production",
    num_workers=8
)

code = generate_python_source(
    Assignment("config", config),
    clean_mode=True
)
print(code)

Clean Mode vs Explicit Mode

Clean mode omits fields that match their default values and must be enabled explicitly:

# Clean mode - concise
code = generate_python_source(assignment, clean_mode=True)
# Output: config = ProcessingConfig(input_path='/data/production', num_workers=8)

Explicit mode includes all fields for complete reproducibility and is the default:

# Explicit mode - complete
code = generate_python_source(assignment, clean_mode=False)
# Output: config = ProcessingConfig(
#     input_path='/data/production',
#     output_path='/data/output',
#     num_workers=8
# )

Working with Enums

Enums are serialized with their full qualified names:

from enum import Enum

class ImageFormat(Enum):
    JPEG = "jpeg"
    PNG = "png"
    TIFF = "tiff"

@dataclass
class ImageConfig:
    format: ImageFormat = ImageFormat.JPEG

config = ImageConfig(format=ImageFormat.PNG)
code = generate_python_source(Assignment("config", config))
# Output includes: from __main__ import ImageFormat
# config = ImageConfig(format=ImageFormat.PNG)

Handling Import Collisions

When multiple modules export the same name, pycodify aliases every colliding import. Aliases are derived from the complete module path, with dots replaced by underscores:

# pycodify generates:
# from package.module_a import Config as Config_package_module_a
# from package.module_b import Config as Config_package_module_b
# left = Config_package_module_a(...)
# right = Config_package_module_b(...)

resolve_imports sorts modules and imported names before emitting them, so a given set of import requirements has stable ordering and aliases. Whole-file byte-for-byte stability additionally depends on value iteration order, repr implementations, and any custom formatters.

Formatting one value

to_source returns a SourceFragment rather than a complete file. Pass a FormatContext when indentation, clean mode, or already-resolved names must be controlled explicitly:

from pycodify import FormatContext, to_source

context = FormatContext(indent=1, clean_mode=True)
fragment = to_source([1, 2], context)
print(fragment.code)
print(fragment.imports)

FormatContext.indented() returns a copy with one additional indentation level. name_mappings maps (module, imported_name) pairs to the local names chosen by resolve_imports; callers normally let generate_python_source populate it during the second pass.

Typed rendering extensions

A host can attach immutable rendering context without adding host-specific state to pycodify. Keys are exact nominal types, and the same context reaches both render passes:

from dataclasses import dataclass
from types import MappingProxyType

from pycodify import (
    Assignment,
    FormatContext,
    SourceFormatter,
    SourceFragment,
    generate_python_source,
)

@dataclass(frozen=True)
class RenderPolicy:
    marker: str

class PolicyValue:
    pass

class PolicyValueFormatter(SourceFormatter):
    priority = 200

    def can_format(self, value):
        return isinstance(value, PolicyValue)

    def format(self, value, context):
        del value
        policy = context.extension(RenderPolicy)
        if policy is None:
            raise RuntimeError("RenderPolicy is required")
        return SourceFragment(repr(policy.marker))

context = FormatContext(
    clean_mode=True,
    extensions=MappingProxyType(
        {RenderPolicy: RenderPolicy(marker="preserved")}
    ),
)
source = generate_python_source(
    Assignment("value", PolicyValue()),
    context=context,
)
assert source == "value = 'preserved'"

extension(RenderPolicy) does not return a value stored under a base class or string key. This exact-type lookup keeps extension ownership explicit. Treat the extension mapping as immutable; use a new FormatContext when the host needs different state.

Nested Dataclasses

Nested dataclasses are properly serialized with all necessary imports:

from dataclasses import dataclass, field

@dataclass
class DatabaseConfig:
    host: str = "localhost"
    port: int = 5432

@dataclass
class AppConfig:
    database: DatabaseConfig = field(default_factory=DatabaseConfig)
    debug: bool = False

config = AppConfig(
    database=DatabaseConfig(host="prod.db.internal"),
    debug=True
)

code = generate_python_source(Assignment("config", config))
# Generates imports for both DatabaseConfig and AppConfig

Executing Generated Code

The generated code is executable Python:

code = generate_python_source(assignment)
namespace = {}
exec(code, namespace)
recreated_config = namespace["config"]
assert recreated_config == original_config

Warning

Generated output is executable Python, not a sandboxed data format. Execute it only when the input objects, custom formatters, header, and resulting file are trusted and have been reviewed. pycodify does not make exec safe for untrusted input.

Replay compatibility is owned jointly by the generated imports and the APIs they call. Explicit mode records default-valued fields, but neither mode can guarantee replay across incompatible module moves or constructor changes.