Final StretchOrdered learning track

Library and Framework Design: Membuat Python Code yang Enak Dipakai

Part 032 — Library and Framework Design: Membuat Python Code yang Enak Dipakai

Membahas desain library/framework Python: public API, ergonomics, extension points, versioning, backward compatibility, deprecation, documentation, typing, errors, packaging, dan design principles.

13 min read2479 words
PrevNext
Lesson 3235 lesson track3035 Final Stretch
#python#library-design#api-design#framework-design+2 more

Part 032 — Library and Framework Design: Membuat Python Code yang Enak Dipakai

1. Tujuan Part Ini

Top-tier Python engineer tidak hanya menulis kode yang bekerja. Mereka menulis kode yang enak dipakai oleh engineer lain.

Library/framework design adalah seni membuat API yang:

  • jelas;
  • stabil;
  • mudah ditemukan;
  • mudah dites;
  • mudah di-debug;
  • typed dengan baik;
  • punya error yang membantu;
  • punya defaults yang aman;
  • extensible tanpa terlalu abstrak;
  • backward-compatible sejauh mungkin;
  • bisa didepresiasi dengan rapi;
  • terdokumentasi;
  • tidak mengejutkan.

Banyak codebase internal gagal karena API internal buruk:

  • function terlalu banyak parameter;
  • error generik;
  • global state tersembunyi;
  • dependency dipakai secara implisit;
  • behavior berubah diam-diam;
  • public/private boundary kabur;
  • docs tidak sesuai;
  • type hints tidak ada;
  • extension point tidak jelas;
  • konfigurasi tersebar;
  • test user experience tidak pernah dipikirkan.

Part ini membahas desain Python API dari sudut pemakai.

Target setelah part ini:

  1. Memahami public API vs internal API.
  2. Mendesain function/class yang ergonomis.
  3. Mendesain defaults yang aman.
  4. Mendesain error hierarchy library.
  5. Mendesain extension points.
  6. Memahami backward compatibility.
  7. Memahami deprecation dengan warnings.
  8. Memahami semantic versioning secara praktis.
  9. Menulis documentation dan examples.
  10. Mendesain typed API.
  11. Mendesain package exports.
  12. Menerapkan prinsip ke case-tracker sebagai library/API internal.

2. API Design Starts from the User

Pertanyaan pertama:

Siapa user API ini?

Kemungkinan user:

  • engineer tim sendiri;
  • tim lain;
  • plugin author;
  • CLI user;
  • API client;
  • internal service;
  • future you;
  • open source user.

API untuk domain internal berbeda dari public PyPI package.

Contoh user story:

As an application developer, I want to create a case and transition its status without knowing how it is stored.

API ideal:

service = CaseService(repository)

case = service.create_case("Late reporting")
service.transition_case(case.id, CaseStatus.SUBMITTED)

Jika user harus tahu JSON path, domain transition table, and storage format, API boundary gagal.


3. Public API vs Internal API

Public API adalah yang kamu janjikan relatif stabil.

Internal API boleh berubah.

Python convention:

def public_function():
    ...


def _internal_helper():
    ...

Module-level underscore menandakan internal.

Package export:

# case_tracker/__init__.py
from case_tracker.domain import Case, CaseId, CaseStatus
from case_tracker.service import CaseService

__all__ = ["Case", "CaseId", "CaseStatus", "CaseService"]

Hati-hati: sesuatu yang diekspor dari __init__.py terasa public.

Rule:

Jangan mengekspos terlalu banyak terlalu awal. Public API adalah janji.


4. API Surface Area

Surface area adalah jumlah hal yang user harus pahami.

Too small:

run_everything(config)

User tidak punya kontrol.

Too large:

CaseFactoryBuilderTransitionManagerRepositoryAdapterConfigurableExecutor(...)

User kewalahan.

Good API exposes:

  • common path simple;
  • advanced path possible;
  • internals hidden;
  • names domain-specific;
  • sensible defaults;
  • explicit extension points.

5. Progressive Disclosure

Progressive disclosure berarti API mudah untuk basic use, tetapi tetap mendukung advanced use.

Simple:

tracker = CaseTracker.in_memory()
case = tracker.create_case("Late reporting")

Advanced:

repository = SqlAlchemyCaseRepository(session_factory)
clock = SystemClock()
id_factory = UuidCaseIdFactory()

tracker = CaseTracker(
    repository=repository,
    clock=clock,
    id_factory=id_factory,
)

Basic user tidak perlu tahu semua dependency. Advanced user bisa mengontrol.


6. Good Defaults

Good defaults reduce friction.

Example:

def create_case(
    title: str,
    *,
    priority: CasePriority = CasePriority.MEDIUM,
) -> Case:
    ...

Bad default:

def create_case(title: str, notes: list[str] = []):
    ...

Mutable default bug.

Good defaults should be:

  • safe;
  • common;
  • explicit enough;
  • not surprising;
  • not hiding production risk.

Dangerous default example:

debug=True

in production-facing API.


7. Keyword-Only Parameters

Use keyword-only for clarity.

def transition_case(
    case_id: CaseId,
    *,
    target_status: CaseStatus,
    actor_id: ActorId,
    reason: str | None = None,
) -> Case:
    ...

Call:

service.transition_case(
    case_id,
    target_status=CaseStatus.SUBMITTED,
    actor_id=actor_id,
    reason="Intake complete",
)

This avoids argument order confusion.

Use positional for obvious primary inputs. Use keyword-only for options/config/policies.


8. Boolean Parameter Smell

Bad:

def list_cases(include_closed: bool) -> list[Case]:
    ...

Call site unclear:

list_cases(True)

Better:

def list_cases(*, include_closed: bool = False) -> list[Case]:
    ...

Better for multiple modes:

class CaseListMode(Enum):
    ACTIVE_ONLY = "active_only"
    INCLUDE_CLOSED = "include_closed"


def list_cases(*, mode: CaseListMode = CaseListMode.ACTIVE_ONLY) -> list[Case]:
    ...

Boolean flags can be okay, but make them keyword-only.


9. Return Types Matter

Design return values deliberately.

Create case:

def create_case(title: str) -> Case:
    ...

Delete:

def delete_case(case_id: CaseId) -> None:
    ...

Batch import:

@dataclass(frozen=True)
class ImportResult:
    imported_count: int
    skipped_count: int
    errors: list[ImportErrorDetail]


def import_cases(rows: Iterable[CaseRow]) -> ImportResult:
    ...

Avoid ambiguous tuple:

return imported, skipped, errors

Named result object improves readability and evolution.


10. Error Design for Libraries

Library should define meaningful exception hierarchy.

class CaseTrackerError(Exception):
    """Base class for case tracker errors."""


class CaseNotFoundError(CaseTrackerError):
    ...


class InvalidCaseTransitionError(CaseTrackerError):
    ...


class CaseStoreError(CaseTrackerError):
    ...

Benefits:

  • users can catch broad or specific errors;
  • errors have semantic meaning;
  • errors can carry structured attributes;
  • API docs can list failure modes.

Error should include useful attributes:

class CaseNotFoundError(CaseTrackerError):
    def __init__(self, case_id: CaseId) -> None:
        self.case_id = case_id
        super().__init__(f"Case not found: {case_id}")

11. Do Not Hide Root Cause

When wrapping errors:

try:
    data = json.loads(raw)
except json.JSONDecodeError as error:
    raise CaseStoreCorruptedError(path, "Invalid JSON") from error

User can inspect cause if needed.

For public API, document:

  • what exception can be raised;
  • when;
  • what attributes it has;
  • whether operation is partially applied.

12. Sync and Async APIs

Do not mix sync/async casually.

Options:

12.1 Sync library

case = client.get_case(case_id)

12.2 Async library

case = await async_client.get_case(case_id)

12.3 Both, separate classes

class CaseClient:
    def get_case(...): ...


class AsyncCaseClient:
    async def get_case(...): ...

Avoid:

def get_case(...):
    return asyncio.run(self.async_get_case(...))

deep inside library. This breaks users already running event loop.


13. Extension Points

Extension point is where user can customize behavior.

Examples:

  • repository protocol;
  • id factory;
  • clock;
  • validator;
  • event handler;
  • logger;
  • retry policy;
  • serialization adapter.

Protocol:

class CaseRepository(Protocol):
    def get(self, case_id: CaseId) -> Case:
        ...

    def save(self, case: Case) -> None:
        ...

User implementation:

class RedisCaseRepository:
    ...

Good extension point:

  • small;
  • documented;
  • stable;
  • behavior contract clear;
  • tested with contract tests.

Bad extension point:

  • too many methods;
  • unclear lifecycle;
  • relies on internal state;
  • changes frequently;
  • undocumented order of calls.

14. Hooks and Callbacks

Callback:

CaseCreatedHandler = Callable[[Case], None]


class CaseTracker:
    def __init__(self, on_case_created: CaseCreatedHandler | None = None) -> None:
        self._on_case_created = on_case_created

Use:

tracker = CaseTracker(on_case_created=lambda case: print(case.id))

Callbacks are simple but can become messy.

For multiple events, event bus pattern:

class EventHandler(Protocol):
    def handle(self, event: DomainEvent) -> None:
        ...

Keep event contracts stable.


15. Avoid Global State

Bad:

DEFAULT_REPOSITORY = JsonCaseRepository(Path("cases.json"))


def create_case(title: str) -> Case:
    return DEFAULT_REPOSITORY.create(title)

Problems:

  • hard to test;
  • environment-specific;
  • thread/concurrency issues;
  • hidden dependency;
  • lifecycle unclear.

Better:

service = CaseService(repository)
service.create_case(title)

Provide convenience factory if needed:

def create_default_service(path: Path = Path("cases.json")) -> CaseService:
    return CaseService(JsonCaseRepository(path))

Convenience should not hide core design.


16. Configuration API

Bad:

configure({"x": 1, "y": True, "z": None})

Better:

@dataclass(frozen=True)
class CaseTrackerConfig:
    store_path: Path
    enable_audit: bool = True
    default_page_size: int = 100

Typed config helps:

  • docs;
  • IDE;
  • validation;
  • defaults;
  • evolution.

For public libraries, avoid requiring huge config object for simple use.


17. Documentation as API

Docs are part of API.

Minimum docs:

  • quickstart;
  • install;
  • basic example;
  • configuration;
  • error handling;
  • extension points;
  • API reference;
  • migration/deprecation notes;
  • troubleshooting.

Good quickstart:

from case_tracker import CaseService, JsonCaseRepository

repository = JsonCaseRepository("cases.json")
service = CaseService(repository)

case = service.create_case("Late reporting")
service.transition_case(case.id, CaseStatus.SUBMITTED)

If quickstart is hard, API is probably hard.


18. Examples as Tests

Examples should run.

Use doctest or normal tests.

Example test:

def test_readme_quickstart(tmp_path) -> None:
    repository = JsonCaseRepository(tmp_path / "cases.json")
    service = CaseService(repository)

    case = service.create_case("Late reporting")

    assert case.status is CaseStatus.DRAFT

Keep docs and tests aligned.


19. Type Hints as User Experience

Good library APIs are typed.

def transition_case(
    self,
    case_id: CaseId,
    target_status: CaseStatus,
) -> Case:
    ...

Types help:

  • autocomplete;
  • static checking;
  • docs;
  • refactoring;
  • protocol implementation.

Avoid exposing Any unnecessarily.

If using Pydantic/FastAPI at boundary, do not let framework-specific types leak into core API unless core API is framework-specific.


20. Backward Compatibility

Backward compatibility means old user code keeps working.

Breaking changes:

  • remove function/class;
  • rename parameter;
  • change return type;
  • change exception type;
  • change default behavior;
  • make optional argument required;
  • change serialized format without migration;
  • change public attribute type;
  • make sync API async;
  • change import path.

Non-breaking or lower-risk changes:

  • add optional keyword-only parameter with default;
  • add new method;
  • add new exception subclass under same base;
  • add new response field if clients tolerate;
  • improve error message with caution;
  • add new enum value if clients handle unknowns.

Compatibility depends on contract. If you did not define contract, users infer one.


21. Semantic Versioning

For library:

MAJOR.MINOR.PATCH
  • MAJOR: breaking changes.
  • MINOR: backward-compatible features.
  • PATCH: backward-compatible bug fixes.

For internal packages, semver can still help.

But semver only works if:

  • public API is defined;
  • changelog exists;
  • deprecations are communicated;
  • tests cover compatibility.

Do not claim semver while breaking users in patch releases.


22. Deprecation

Deprecation is a transition period before removal.

Use warnings.warn.

import warnings


def old_create_case(title: str) -> Case:
    warnings.warn(
        "old_create_case() is deprecated; use create_case() instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    return create_case(title)

stacklevel=2 points warning at caller.

Deprecation plan:

  1. Add new API.
  2. Keep old API working.
  3. Emit warning.
  4. Document migration.
  5. Wait appropriate releases/time.
  6. Remove in major version or announced release.
  7. Provide changelog.

23. Warning Categories

Common:

  • DeprecationWarning;
  • FutureWarning;
  • UserWarning.

For library user-facing deprecations, warning visibility depends on context. Tests can turn warnings into errors:

python -W error::DeprecationWarning -m pytest

Use warnings carefully. Too many warnings create noise.


24. Changelog

Keep changelog:

# Changelog

## 0.3.0

### Added
- Added `CaseService.transition_case`.

### Deprecated
- Deprecated `old_transition_case`.

### Fixed
- Fixed duplicate case id detection.

### Breaking
- None.

Changelog helps:

  • upgrade decisions;
  • incident investigation;
  • compatibility review;
  • user trust.

25. API Review Checklist

Before making API public, ask:

  1. Is name clear?
  2. Is common path simple?
  3. Are advanced options possible?
  4. Are defaults safe?
  5. Are parameters typed?
  6. Are boolean flags keyword-only?
  7. Is return type explicit?
  8. Are errors documented?
  9. Are partial failure semantics clear?
  10. Is dependency hidden or explicit?
  11. Is extension point stable?
  12. Is behavior testable?
  13. Is docs example short?
  14. Can this evolve backward-compatibly?
  15. What would users likely misuse?

26. Fluent API vs Plain API

Fluent:

query = (
    CaseQuery()
    .with_status(CaseStatus.SUBMITTED)
    .assigned_to(reviewer_id)
    .limit(100)
)

Plain:

query = CaseQuery(
    status=CaseStatus.SUBMITTED,
    assigned_to=reviewer_id,
    limit=100,
)

Fluent can be nice for complex builder patterns. Plain dataclass is often clearer in Python.

Do not build fluent API just to look fancy.


27. Context Managers as API

For resources:

with CaseTracker.open("cases.db") as tracker:
    tracker.create_case("Late reporting")

Implement:

class CaseTracker:
    def __enter__(self) -> "CaseTracker":
        self.open()
        return self

    def __exit__(self, exc_type, exc, tb) -> None:
        self.close()

Use context managers for:

  • DB sessions;
  • transactions;
  • file resources;
  • temporary settings;
  • locks.

Context manager makes lifecycle explicit.


28. Transaction API Design

Bad:

service.create_case(...)
service.save()

Unclear lifecycle.

Better:

with unit_of_work:
    service.create_case(...)

or:

service.create_case(...)

where service owns transaction boundary.

For libraries, document:

  • who commits;
  • what happens on exception;
  • whether operation is atomic;
  • whether object remains usable after rollback.

29. Framework Design: Inversion of Control

Framework calls user code.

Library is called by user code.

Library:

result = library.do_work(input)

Framework:

@app.route(...)
def user_handler():
    ...

Framework design must define:

  • lifecycle;
  • hook order;
  • error handling;
  • dependency injection;
  • extension points;
  • configuration;
  • plugin discovery;
  • backwards compatibility.

Frameworks are harder to design than libraries because they own more control flow.


30. Plugin Design

Plugin interface:

class CasePlugin(Protocol):
    name: str

    def on_case_created(self, case: Case) -> None:
        ...

Plugin registry:

class PluginRegistry:
    def __init__(self) -> None:
        self._plugins: list[CasePlugin] = []

    def register(self, plugin: CasePlugin) -> None:
        self._plugins.append(plugin)

Questions:

  • sync or async hooks?
  • error isolation?
  • hook order?
  • plugin config?
  • version compatibility?
  • timeout?
  • security?
  • can plugin mutate domain object?
  • are hooks transactional?

Plugin systems need careful contracts.


31. Error Isolation in Hooks

If plugin fails, should main operation fail?

Option A: fail operation.

for plugin in plugins:
    plugin.on_case_created(case)

Option B: log and continue.

for plugin in plugins:
    try:
        plugin.on_case_created(case)
    except Exception:
        logger.exception("Plugin failed")

Option C: collect failures.

Document behavior.

For audit/security hooks, failure may need to fail operation. For optional notification hook, log-and-continue may be okay.


32. Package Layout for Public API

Internal layout:

case_tracker/
  __init__.py
  domain.py
  service.py
  repository.py
  _internal.py

__init__.py:

from case_tracker.domain import Case, CaseId, CaseStatus
from case_tracker.errors import CaseTrackerError, CaseNotFoundError, InvalidCaseTransitionError
from case_tracker.service import CaseService

__all__ = [
    "Case",
    "CaseId",
    "CaseStatus",
    "CaseService",
    "CaseTrackerError",
    "CaseNotFoundError",
    "InvalidCaseTransitionError",
]

This lets users:

from case_tracker import CaseService, CaseStatus

But avoid making import heavy. __init__.py should not connect database or load config.


33. __all__ and Discoverability

__all__ documents intended export names.

It affects wildcard import:

from case_tracker import *

Even if wildcard import is discouraged, __all__ helps communicate public API.

Keep it curated.


34. Module Names

Good module names:

domain
errors
service
repository
config
events

Bad:

utils
helpers
misc
manager
stuff

If using utils, be specific:

time_utils
json_utils

But often a domain-specific name is better.


35. Avoid API Surprise

Surprising behavior:

case = service.get_case(case_id)
case.status = CaseStatus.CLOSED
# silently persists?

Better:

  • mutation explicit;
  • persistence explicit;
  • method documents side effect.

Example:

service.transition_case(case_id, CaseStatus.CLOSED)

For returned domain objects, document whether changes are tracked/persisted or detached copies.

ORMs often surprise users here. Service APIs should be explicit.


36. Stable Serialization Contracts

If your library emits JSON/data:

{
  "id": "CASE-001",
  "status": "DRAFT"
}

Clients may depend on field names.

Changing status to case_status is breaking.

Strategies:

  • version schema;
  • add field before removing;
  • support aliases during transition;
  • document stability;
  • provide migration guide.

37. Testing API Compatibility

Add tests for public API imports:

def test_public_api_exports() -> None:
    from case_tracker import CaseService, CaseStatus

    assert CaseService is not None
    assert CaseStatus.DRAFT.value == "DRAFT"

Test deprecation warning:

def test_old_create_case_warns() -> None:
    with pytest.warns(DeprecationWarning):
        old_create_case("Late reporting")

Test examples from docs.


38. Contract Tests for Extension Points

For repository plugin:

def assert_case_repository_contract(repository: CaseRepository) -> None:
    ...

All implementations must pass.

For event handler:

def assert_event_handler_contract(handler: EventHandler) -> None:
    ...

Contract tests help external/internal implementers.


39. Case Tracker Public API Proposal

from case_tracker import (
    Case,
    CaseId,
    CaseService,
    CaseStatus,
    CaseTrackerError,
    InvalidCaseTransitionError,
    JsonCaseRepository,
)

Simple use:

repository = JsonCaseRepository("cases.json")
service = CaseService(repository)

case = service.create_case("Late reporting")
service.transition_case(case.id, CaseStatus.SUBMITTED)

Convenience:

service = CaseService.from_json_file("cases.json")

But avoid hiding too much.


40. Case Tracker Deprecation Example

Old API:

def create_new_case(path: Path, title: str) -> Case:
    ...

New API:

service = CaseService(JsonCaseRepository(path))
service.create_case(title)

Deprecation wrapper:

def create_new_case(path: Path, title: str) -> Case:
    warnings.warn(
        "create_new_case(path, title) is deprecated; use CaseService(JsonCaseRepository(path)).create_case(title).",
        DeprecationWarning,
        stacklevel=2,
    )
    return CaseService(JsonCaseRepository(path)).create_case(title)

Test warning.


41. Case Tracker Extension Points

Potential extension points:

ExtensionInterface
RepositoryCaseRepository Protocol
ClockClock Protocol
ID factoryCallable[[], CaseId]
Event handlerDomainEventHandler Protocol
Authorization policyCasePolicy Protocol
SerializerCaseSerializer Protocol
MetricsMetrics Protocol

Do not add all at once. Add when requirement appears.


42. API Design Smell Checklist

Watch for:

  1. Public API too large.
  2. Underscore internals imported by examples.
  3. Mutable default arguments.
  4. Positional boolean flags.
  5. Return tuple with unclear fields.
  6. Generic Exception.
  7. Error without useful attributes.
  8. Global hidden repository/config.
  9. Function does I/O unexpectedly.
  10. Domain API depends on framework.
  11. Sync API secretly runs event loop.
  12. Extension point undocumented.
  13. Breaking change in patch release.
  14. Deprecation without migration path.
  15. Docs example does not run.
  16. Type hints missing from public API.
  17. __init__.py causes heavy side effects.
  18. Plugin failure behavior undefined.
  19. User must understand internal storage format.
  20. Convenience API hides unsafe defaults.

43. Practice: Define Public API

Write case_tracker/__init__.py.

Decide exports:

  • Case;
  • CaseId;
  • CaseStatus;
  • CaseService;
  • JsonCaseRepository;
  • CaseTrackerError;
  • CaseNotFoundError;
  • InvalidCaseTransitionError.

Add __all__.

Test public imports.


44. Practice: Improve Function Signature

Before:

def transition_case(case_id, status, actor, notify, audit, reason=None):
    ...

After:

def transition_case(
    case_id: CaseId,
    *,
    target_status: CaseStatus,
    actor_id: ActorId,
    reason: str | None = None,
    notify: bool = True,
    audit: bool = True,
) -> Case:
    ...

Discuss whether notify/audit should be policy/config instead.


45. Practice: Add Deprecation Warning

Deprecate old function and test:

with pytest.warns(DeprecationWarning, match="deprecated"):
    old_function()

Run tests with warnings as errors in CI periodically.


46. Practice: Write Quickstart

Create docs/quickstart.md with:

  1. Install.
  2. Create repository.
  3. Create service.
  4. Create case.
  5. Transition case.
  6. Handle errors.
  7. Run tests.

Then write a test that follows quickstart code.


47. Practice: Extension Contract

Define CaseRepository contract test. Run for:

  • fake repository;
  • JSON repository;
  • SQLite repository if available.

Document contract in docs/repository-contract.md.


48. Self-Check

Jawab tanpa melihat materi:

  1. Apa itu public API?
  2. Apa itu internal API?
  3. Kenapa public API adalah janji?
  4. Apa itu surface area?
  5. Apa itu progressive disclosure?
  6. Kenapa defaults harus aman?
  7. Kenapa keyword-only parameter membantu?
  8. Kenapa positional boolean flag buruk?
  9. Kapan memakai result object?
  10. Bagaimana mendesain exception library?
  11. Kenapa root cause harus dipertahankan?
  12. Apa beda sync dan async API?
  13. Apa itu extension point?
  14. Apa risiko global state?
  15. Kenapa docs adalah API?
  16. Apa itu backward compatibility?
  17. Apa itu semantic versioning?
  18. Bagaimana deprecation yang baik?
  19. Kenapa stacklevel=2 penting?
  20. Apa API smell paling berbahaya?

49. Definition of Done Part 032

Kamu selesai part ini jika bisa:

  1. Menentukan public exports.
  2. Menulis __all__.
  3. Mendesain simple common path API.
  4. Menyediakan advanced dependency injection path.
  5. Membuat defaults aman.
  6. Menggunakan keyword-only options.
  7. Menghindari positional boolean flags.
  8. Membuat result object.
  9. Membuat exception hierarchy.
  10. Mendokumentasikan extension point.
  11. Menulis deprecation warning.
  12. Menulis changelog entry.
  13. Menulis quickstart.
  14. Mengetes public imports.
  15. Menjelaskan backward compatibility impact.

50. Ringkasan

Library design adalah user experience untuk engineer.

Inti part ini:

  • public API adalah janji;
  • internal API harus jelas dibedakan;
  • common path harus mudah, advanced path tetap mungkin;
  • defaults harus aman dan tidak mengejutkan;
  • keyword-only parameter meningkatkan clarity;
  • boolean flags harus hati-hati;
  • return type harus eksplisit dan evolvable;
  • exception hierarchy adalah bagian dari API;
  • root cause harus dipertahankan;
  • extension points harus kecil, stabil, terdokumentasi, dan dites;
  • global state membuat API sulit dites dan dioperasikan;
  • docs dan examples adalah bagian dari API;
  • backward compatibility harus dikelola;
  • deprecation butuh warning, docs, migration path, dan waktu;
  • framework design lebih sulit karena mengontrol lifecycle user code.

Part berikutnya membahas migration, modernization, dan maintaining Python systems.


51. Referensi

  • Python Documentation — warnings.
  • Python Documentation — typing.Protocol.
  • Python Documentation — dataclasses.
  • Python Packaging User Guide.
  • Python Documentation — __all__ and modules.
  • Python Documentation — contextlib.
Lesson Recap

You just completed lesson 32 in final stretch. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.