PEP 843 – DRY 재수출을 위한 Export 문
- Author:
- Neil Girdhar <mistersheik at gmail.com>
- Sponsor:
- Peter Bierma <peter at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Created:
- 05-Aug-2026
- Python-Version:
- 3.16
- Post-History:
- 05-Aug-2026, 21-Aug-2026
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
대규모 라이브러리는 구현 레이아웃(유지 관리자가 편리하게 사용할 수 있는 모듈 트리)과 공개 레이아웃(사용자에게 제공하는 더 얕고 선별된 트리)을 분리합니다. 오늘날 이러한 공개 레이아웃을 구축하려면 불완전한 두 가지 선택지 중 하나를 선택해야 합니다.
첫 번째는 내보내는 모든 이름을 두 번 작성하는 것입니다. 한 번은 import 문에 작성하고, 다시 __all__에 문자열로 작성합니다. 공개 레이아웃이 변경될 때마다 두 목록을 수작업으로 동기화해야 하므로 DRY (Don’t Repeat Yourself)를 위반합니다. “모든 지식은 시스템 안에서 단 하나의 명확하고 모호하지 않으며 권위 있는 표현을 가져야 합니다.” [1]
두 번째는 재귀적 별칭 관용구인 from x import y as y입니다. 이것은 타입 시스템의 일부입니다. 타입 검사기는 이를 해당 import가 의도적인 재수출임을 나타내는 신호로 취급합니다. 이 관례를 모르는 사람에게는 여전히 오타처럼 보입니다. 모듈에 선별된 __all__이 없기 때문에, 실제 __all__이 제공하는 와일드카드 import 제어 기능을 잃습니다.
이 PEP는 두 문제를 모두 피하는 문 형식을 추가합니다.
# spam/__init__.py
from ._internal.core export PublicAPI
from ._internal.widgets export Widget as PublicWidget
이 문은 from ... import ... as ...와 정확히 같은 방식으로 이름을 별칭 아래에서 선택적으로 import하고, 동일한 문에서 해당 이름을 __all__에 추가합니다. 더 이상 수작업으로 동기화할 것이 없으며, 별칭을 해석할 필요도 없습니다.
PEP 842와의 관계
두 PEP는 모두 __all__에 대한 동일한 불편함에서 출발하며, 재수출을 위한 동일한 핵심 메커니즘, 즉 from <module> export <name> 형태의 문에 동의합니다. 여기에는 지연 변형도 있습니다(Lazy exports 참조). 독립적으로 도달한 이러한 합의는 이것이 재수출에 적합한 형태임을 확인해 줍니다.
PEP 842는 이 메커니즘을 다섯 가지 형식에서 사용할 수 있는 키워드로 확장합니다.
- 독립형
export NAME export NAME = VALUE할당export defexport class- 모듈 재수출 문인
from MODULE export NAME
다섯 형식 모두 __export__를 채우며, 이는 __all__이 되기도 하고 제외된 항목에 접근할 때 ExportError를 발생시킵니다.
PEP 842의 버전에는 Wildcard form에 해당하는 와일드카드 형식이 없습니다. 이 PEP는 나머지 형식을 의도적으로 제외하고 모듈 재수출 문만 취합니다. 무엇을, 왜 제외하는지는 Non-goals를 참조하십시오.
동기
NumPy, pandas, polars, Typer, FastAPI, Plotly와 같이 널리 사용되는 라이브러리는 거의 예외 없이 공통 패턴을 사용하여 공개 API를 내보냅니다. 최상위 __init__.py에는 비공개 서브모듈에서 가져오는 import가 빽빽하게 들어 있으며, 그 뒤에 동일한 이름을 문자열로 반복하는 __all__ 목록이 오거나 이 목록과 뒤섞여 있습니다. 또는 그 대신 재귀적 별칭 관용구를 전체적으로 사용합니다. pandas와 polars는 모두 이중 목록 형식을 포함하며, FastAPI와 Typer는 모두 전체적으로 재귀적 별칭 형식을 사용합니다. 첫 번째 형식은 다음과 같습니다.
from ._internal.core import PublicAPI as PublicAPI
from ._internal.widgets import Widget as Widget
from ._internal.errors import SpamError as SpamError
# ... often hundreds of lines like this ...
__all__ = [
"PublicAPI",
"Widget",
"SpamError",
# ... the same names again ...
]
이 파일에서 라이브러리는 구현 레이아웃을 공개 레이아웃으로 평탄화합니다. 라이브러리가 성장하면 두 레이아웃은 서로 달라집니다. 유지 관리자의 편의를 위해 코드가 서브모듈로 재구성되는 동안에도 공개 레이아웃은 사용자에게 안정적으로 유지됩니다.
무언가가 평탄화를 수행해야 합니다. 오늘날 그 무언가는 수작업으로 관리되는 두 번 작성된 목록입니다. 즉, 내보내기 목록(__all__에 있는 목록)과 가져오기 목록(import 문에 있는 목록)이 같은 내용을 두 번 말합니다. 이름 변경, 추가 또는 제거를 할 때마다 두 곳을 수작업으로 수정해야 하며, 두 목록은 아무런 경고 없이 서로 달라질 수 있습니다. 이 PEP는 두 목록을 하나의 from <module> export <name> 문으로 합쳐 이러한 중복을 제거합니다.
대안인 import x as x는 언어에 export 개념이 없어서 사용하는 우회 방법이며, 일부 자동 포매터는 단순한 import x를 사용되지 않는 것으로 보고 제거하기 때문에 여전히 문제가 발생합니다.
__all__은 두 가지 관심사를 뒤섞습니다
수작업으로 관리되는 __all__은 한 파일에서 서로 다른 두 가지 관심사도 섞습니다. 하나는 가져오기 목록(평탄화가 연결되는 방식의 구현 세부 사항)이고, 다른 하나는 공개 API의 선언(사용자에 대한 약속)입니다. 두 목록은 파일의 서로 다른 위치에 있는 별도의 문에 존재하며, 검토자가 서로 대조하여 직접 확인하거나 이 경우를 정확히 위해 만들어진 린터 규칙을 사용하는 것 외에는 어느 것도 두 목록을 동기화하지 않습니다. 더 흔한 “사용되지 않는 가져오기” 검사는 도움이 되지 않습니다. 이름을 가져왔지만 __all__에 추가하지 않으면 해당 이름은 사용되지 않는 것으로 보이며, 자동 수정 도구는 누락을 드러내기보다 일상적으로 그 이름을 삭제합니다.
밑줄은 다른 문제를 해결합니다
자연스러운 대응은 “내부 이름 앞에 밑줄을 붙이면 됩니다”입니다. 그러나 이 PEP가 중요하게 여기는 비공개성은 이름 수준이 아니라 패키지 수준에 있습니다. 즉, 대규모 다중 모듈 패키지의 어느 부분을 public layout에 포함할 것인지의 문제입니다. 이름 앞에 밑줄을 붙이는 방식은 이미 모듈 안에서 해당 이름을 비공개로 표시합니다.
이 문제는 내부 모듈에서 이름을 모아 안정적인 공개 이름으로 제공하는 일만을 담당하는 “허브” 모듈(대개 __init__.py 파일)에서 나타납니다. 허브 모듈에 도달하는 모든 이름은 이미 공개될 예정입니다. 즉, 이름이 허브에 들어올 때쯤이면 밑줄 규칙은 이미 그 역할을 다한 것입니다. 허브 모듈에는 “이것도 패키지의 공개 레이아웃의 일부입니다”라고 반복 없이 표현할 방법이 필요합니다.
목표가 아닌 사항
이 PEP는 다음을 목표로 하지 않습니다:
- 내보내지 않은 이름에 대한 런타임 속성 접근을 제한하거나
__getattr__의 의미를 변경하는 것. Why no runtime enforcement를 참조하십시오. - 서드파티
atpublic패키지가@public/@private데코레이터를 사용하여 수행하는 것처럼, 새로 작성한def,class또는 할당을 해당 정의 위치에서 내보낸 것으로 표시하는 것. Why only re-exports를 참조하십시오.
사양
export는 from import 문에서 import를 대체하는 소프트 키워드입니다:
from ._internal.core export PublicAPI
from ._internal.widgets export Widget as PublicWidget
from numpy.typing export NDArray
from <module> export <name> [as <alias>] 문은 from <module> import <name> [as <alias>] 문과 동일하게 동작합니다. 즉, 현재 네임스페이스에 <name> 또는 지정된 경우 <alias>를 바인딩하고, 해당 이름을 __all__에 추가합니다:
from <module> import <name> as <alias>
exported_names = globals().setdefault("__all__", [])
if not isinstance(exported_names, list):
exported_names = list(exported_names)
__all__ = exported_names
exported_names.append("<alias>")
이 PEP의 다른 모든 문 형식은 __all__을 추가하거나 확장하기 전에 동일한 방식으로 정규화합니다.
export는 일반 import와 __all__에 대한 추가 작업으로 문법적 설탕을 제거할 수 있으므로, 제어 흐름과 정확히 import와 같은 방식으로 결합됩니다:
if sys.platform == "win32":
from ._internal.windows export WindowsThing
else:
from ._internal.posix export PosixThing
각 분기는 자체 import와 자체 __all__추가를 실행하므로, 최종적으로 내보내지는 이름은 별도의 __all__관리 없이 어느 분기가 실행되었는지에 따라 결정됩니다.
import와 달리 export는 모듈 수준으로 제한됩니다. def 또는 class 본문 안에서는 SyntaxError가 발생하지만, 위의 플랫폼 예제처럼 새로운 스코프를 도입하지 않는 if, try, for, while 또는 with 블록 안에는 여전히 나타날 수 있습니다. 이러한 제한이 존재하는 이유는 함수 또는 클래스 본문 안에 바인딩된 이름은 애초에 모듈의 네임스페이스에 속한 적이 없으므로 export가 __all__에 추가할 대상이 없기 때문입니다. export의 핵심 목적은 모듈의 공개 API를 채우는 것이며, 모듈 수준에서 바인딩된 이름만 이에 해당합니다.
특히 모듈 수준의 if typing.TYPE_CHECKING: 가드에서 export를 사용하면 _typeshed와 같은 스텁 전용 패키지가 스텁에는 존재하지만 런타임 대응 항목은 없는 이름을 내보낼 수 있습니다.
if typing.TYPE_CHECKING:
from ._internal.types export InternalOnly
<module>은 일반적인 from ... import ... 문과 정확히 동일하게 상대 경로(from .core export Thing, from ..sub.core export Thing) 또는 절대 경로(from numpy.typing export NDArray)일 수 있습니다. 하나의 문에서 일반적인 여러 이름을 지정하는 from import와 동일한 구문을 사용하여 여러 이름을 내보낼 수 있으며, 긴 목록의 경우 괄호로 묶은 여러 줄 목록도 사용할 수 있습니다:
from ._internal.widgets export Widget, Gadget as PublicGadget
from ._internal.widgets export (
Widget,
Gadget,
Doohickey,
)
이름을 내보내는 것 자체가 그 이름을 사용하는 것이므로, “imported but unused” 이름을 표시하는 도구(린터, 포매터)는 from <module> export ...문으로 바인딩된 모든 이름을 사용된 것으로 처리해야 하며, 이는 해당 도구가 이미 from module import Thing as Thing을 특별히 처리하는 방식과 같습니다. 이 PEP는 해당 도구가 결정하는 내용을 변경하지 않습니다. 이는 export가 의미하는 바에서 비롯됩니다. 내보내기가 곧 사용입니다.
와일드카드 형식
이 제안에는 와일드카드 형식인 from <module> export *도 포함됩니다. 이는 동일한 규칙(<module>자체의 __all__이 정의되어 있으면 이를 사용하고, 그렇지 않으면 밑줄로 시작하지 않는 모든 최상위 이름을 사용함)에 따라 from <module> import *가 바인딩할 모든 이름을 바인딩하고, 해당 이름을 모두 현재 모듈의 __all__에 추가합니다.
# spam/_internal/core.py
__all__ = ["PublicAPI", "Helper"] # curated by the internal module itself
...
# spam/__init__.py
from ._internal.core export *
# binds PublicAPI and Helper, and adds both to spam.__all__
이는 일반적인 2계층 레이아웃을 지원합니다. 내부 모듈은 작성되는 시점에 자체 __all__을 선별하고, 허브는 각 항목의 이름을 다시 지정하는 대신 하나의 문으로 해당 목록 전체를 다시 내보냅니다.
export *는 <module>이 자체 __all__을 정의하지 않은 경우 import *의 대체 동작과 일치합니다. 밑줄로 시작하지 않는 모든 최상위 이름을 내보냅니다.
와일드카드 형식은 Specification 에서와 같이 __all__을 정규화하면 다음과 동등합니다.
# from ._internal.core export *
from ._internal.core import *
__all__.extend(_names_bound_by_star_import)
여기서 _names_bound_by_star_import은 방금 from ._internal.core import *로 바인딩된 이름의 목록이며, Python의 가져오기 메커니즘이 와일드카드 가져오기를 실행하기 위해 이미 계산하는 동일한 목록입니다.
지연 내보내기
PEP 810은 가져온 이름이 처음 사용될 때까지 from ... import문을 지연하는 lazy소프트 키워드를 추가합니다. lazy from <module> import <name>은 즉시 지연 프록시를 바인딩하지만 해당 프록시에 접근할 때까지 <module>을 로드하지 않습니다.
export는 import와 결합하는 것과 동일한 방식으로 이 기능과 결합합니다.
lazy from ._internal.core export PublicAPI
이는 PEP 810에 명시된 대로 PublicAPI를 지연 프록시에 바인딩하고, 프록시에 접근할 때까지 기다리지 않고 즉시 "PublicAPI"를 __all__에 추가합니다. __all__을 채우는 데에는 로드된 값이 아니라 이름을 문자열로 나타낸 것만 필요하므로, 가져오기 부분이 지연되더라도 문의 내보내기 부분은 즉시 실행됩니다. 수백 개를 다시 내보내는 허브 모듈의 경우, 모든 내부 모듈을 미리 로드하는 비용을 지불하지 않고도 가져오기 시점에 사용자에게 완전하고 정확한 __all__과 dir()을 제공합니다.
이 문은 Specification 에서와 같이 __all__을 정규화하면 다음과 동등합니다.
# lazy from ._internal.core export PublicAPI
lazy from ._internal.core import PublicAPI
__all__.append("PublicAPI")
lazy from <module> export *는 서로 독립적인 두 가지 이유로 허용되지 않습니다. PEP 810은 이미 lazy from <module> import *를 허용하지 않으며, 와일드카드 내보내기 형식은 __all__에 어떤 이름이 포함되어 있는지 알아야 하므로 <module>을 로드해야 하는데, 이는 지연 기능이 지연하는 바로 그 작업입니다. lazy는 또한 PEP 810의 범위 제한을 따르므로 모듈 수준에서만 유효하고 함수, 클래스 또는 try블록 내부에서는 유효하지 않습니다.
NumPy의 numpy/__init__.py는 이것이 중요한 이유를 보여 줍니다. 모듈 수준의 __getattr__은 서로 관련 없는 두 가지 작업을 동시에 수행합니다. import numpy시점에 가져오지 않은 서브모듈을 지연 로드하고, 더 이상 존재하지 않는 속성에 대해 유용한 오류를 발생시킵니다.
# numpy/__init__.py, today (abbreviated)
def __getattr__(attr):
# Warn for expired attributes
import warnings
if attr == "linalg":
import numpy.linalg as linalg
return linalg
if attr == "fft":
import numpy.fft as fft
return fft
# ... one branch like this per lazily loaded submodule ...
if attr in __expired_attributes__:
raise AttributeError(f"`np.{attr}` was removed. ...")
raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")
첫 번째 작업만 지연 기능과 관련되며, 지연 내보내기가 이를 직접 대체합니다.
# numpy/__init__.py, with lazy exports
lazy from . export linalg
lazy from . export fft
# ... one statement per lazily loaded submodule ...
def __getattr__(attr):
# Only the expired-attribute branch is left
if attr in __expired_attributes__:
raise AttributeError(f"`np.{attr}` was removed. ...")
raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")
이는 단지 더 짧기만 한 것이 아니라 더 정확합니다. 오늘날 linalg는 __getattr__ 폴백을 통해서만 존재하므로, NumPy가 이를 나열하는 __dir__ 재정의를 별도로 유지하지 않는 한 dir(numpy)와 탭 완성에서 보이지 않습니다. lazy from . export linalg는 실제(지연) 속성을 즉시 바인딩하고 "linalg"를 __all__에 추가하므로, dir()와 __all__이 자동으로 올바르게 설정되며, 이제 일반 속성 조회가 실행되기 전에 성공하므로 이러한 이름에 대해서는 __getattr__이 더 이상 호출되지도 않습니다.
더 이상 전혀 존재하지 않는 속성에 대해 경고하는 두 번째 작업은 export가 다루는 대상이 아닙니다. export는 바인딩되어야 하는 이름에만 관련되며, 제거된 이름에 대해서는 아무것도 규정하지 않습니다. 이를 위해 모듈의 __getattr__는 여전히 필요하지만, 지연 서브모듈 분기가 빠져나간 뒤에는 사용 중단 로직만 남긴 더 작은 형태가 됩니다.
이 재작성에서 중요한 한 가지 제한이 있습니다. PEP 810은 함수 본문 내부의 lazy를 허용하지 않으므로, 지연 서브모듈 분기는 __getattr__내부에서 한 줄씩 대체할 것이 아니라 모듈 최상위 수준으로 이동해야 합니다. 이는 구조 재편이지 즉시 대체할 수 있는 치환이 아니지만, 허브 모듈(How to Teach This) 이 이미 취하고 있는 형태와 정확히 같습니다.
__all__과의 상호 작용
모듈은 from ... export ...문을 수동으로 관리되는 __all__과 자유롭게 섞거나, 파일의 다른 위치에 있는 __all__ += / __all__.append 호출과 섞을 수 있습니다. 각 export문은 새 이름을 추가하기 전에 모듈의 네임스페이스에서 현재 __all__에 바인딩된 값을 확인합니다.
__all__이 아직 존재하지 않으면export는 이를 빈 목록으로 생성합니다.__all__이 존재하지만 아직 리스트가 아니면,export는 기존 내용을 보존하면서 이를 리스트로 복사합니다.- 그렇지 않으면
__all__은 이미 리스트이므로 그대로 사용합니다.
그런 다음 새 이름을 추가합니다. 따라서 export문은 항상 __all__을 일반적인 변경 가능한 리스트로 남기며, 이후 같은 모듈의 코드가 일반 리스트 연산으로 계속 확장할 수 있는 리스트입니다:
from .foo export Foo
__all__ += ["Baz"]
중복된 이름도 허용됩니다. __all__은 중복이 없어야 한다는 요구를 받은 적이 없으며, 이 PEP도 이를 변경하지 않습니다.
from ... export ...는 __all__의 내용에만 영향을 주며, 이는 다시 from module import *와 이미 __all__을 읽는 모든 도구(문서 생성기, 린터, IDE)에 영향을 줍니다.
export는 허브 모듈 자체의 dir()에서 <module>에 명시된 중간 서브모듈을 숨길 수 없습니다. spam/__init__.py에 from ._internal.core export PublicAPI가 포함되어 있으면, 문에서 import, from ... import, export중 무엇을 사용하는지와 관계없이 _internal은 spam 모듈의 속성이 되고 dir(spam)에 나타납니다. spam/__init__.py의 자체 네임스페이스는 바로 spam.__dict__이며, Python의 import 시스템은 패키지와 일반 모듈 모두에서 가져온 서브모듈을 가져오는 과정의 부작용으로 상위 패키지의 네임스페이스에 바인딩합니다. 이는 임포트 시스템의 속성이며, export가 도입하거나 억제할 수 있는 것이 아닙니다. 이는 속성 접근 숨김이 이 PEP의 non-goal인 또 하나의 이유입니다.
의미론적 구현
각 from <module> export <name> as <alias>문은 Specification에 보인 대로 정확히 디슈거링됩니다. 이름을 임포트하고, __all__을 정규화한 다음 추가합니다. 예를 들면 다음과 같습니다:
# from ._internal.core export PublicAPI
from ._internal.core import PublicAPI
__all__.append("PublicAPI")
# from ._internal.widgets export Widget as PublicWidget
from ._internal.widgets import Widget as PublicWidget
__all__.append("PublicWidget")
와일드카드 형식의 대응 표현은 Wildcard form에, 지연 형식의 대응 표현은 Lazy exports에 제시되어 있습니다.
다른 소프트 키워드와 마찬가지로 export는 임포트 문에서 from <module> 바로 뒤인 경우를 제외하면 어디서나 유효한 식별자로 남습니다.
근거
왜 키워드이며 데코레이터가 아닌가
서드파티 atpublic 패키지의 @public 스타일 데코레이터는 개별적으로 정의된 함수와 클래스에는 깔끔하게 작동하지만, import 문과는 결합되지 않습니다. “정의”가 임포트를 통해 모듈에 들어오는 이름일 뿐일 때는 데코레이트할 객체가 없기 때문입니다.
atpublic은 함수 호출 형식인 public(some_imported_name)으로 이 문제를 우회하지만, 이 PEP가 없애는 이중 기록을 다시 도입합니다. 이름을 임포트에서 한 번 쓰고, 다시 public()의 인자로 써야 합니다. 또한 별칭과도 결합되지 않습니다. 함수 호출 형식은 키워드 인자인 public(alias=name)만 받으며, 이는 alias를 __all__에 추가하는 동시에 바인딩합니다. 따라서 별칭을 공개하려면 from x import y as z를 사용하는 대신 호출에서 매핑을 명시적으로 작성해야 합니다. 문 수준의 export 키워드는 임포트 문 자체의 일부이므로 이 두 문제를 모두 피합니다. 어차피 존재할 임포트에 아무것도 추가하지 않습니다.
왜 재내보내기만 지원하는가
이 PEP는 새 def, class, 또는 대입을 정의되는 위치에서 내보내지는 것으로 표시하는 방법을 의도적으로 제외합니다. 일부 소규모 라이브러리와 단일 파일 모듈은 함수가 정의되는 바로 그 위치에 export def public_function(): ...를 작성하는 편을 선호할 수 있지만, 이러한 사용 사례에는 이 PEP가 해결하는 DRY 문제가 없습니다. 이름을 정의하고 같은 위치에서 내보내면 한 번만 작성됩니다. 유지 관리자는 앞에 밑줄을 쓸지 이미 선택하며, atpublic의 @public 데코레이터 같은 도구도 새 문 없이 정의 위치에서 이러한 선택을 할 수 있게 합니다.
공개 구조를 재내보내기 허브를 중심으로 구성하지 않는 소규모 라이브러리와 단일 파일 모듈에는 이 PEP가 전혀 필요하지 않습니다. atpublic이 이미 이러한 용도로 사용됩니다. 반대로 처음부터 허브와 내부 구조를 채택하는 새 프로젝트에는 atpublic을 사용할 이유도 거의 없습니다. 공개될 모든 것이 이미 허브의 export 문을 통해 전달되기 때문입니다. atpublic이 어쨌든 충분히 널리 채택된다면, 그것이나 이와 유사한 무언가는 이 제안과는 독립적으로 언젠가 표준 라이브러리에 포함될 수도 있습니다.
이 PEP를 위해 수집한 근거(Nu mPy, pandas, polars, Typer, FastAPI, Plotly)는 모두 데코레이터를 원하는 개별 정의 이름이 아니라 재내보내기 허브에 관한 것입니다. 익숙한 from ... import ...를 확장하는 단일 문 형식은 def, class, 대입 문에 새로운 접두사 규칙을 가르치는 대신 문법을 설명하고 검토하기 쉽게 유지합니다. 실제 사례에서 이러한 공백에 대한 근거가 나타나면 이후 별도의 PEP에서 정의 위치 표식을 자유롭게 제안할 수 있습니다. 이 PEP는 재내보내기 문제를 해결하기 위해 그 문제까지 해결할 필요는 없습니다.
왜 런타임 적용을 하지 않는가
저자는 런타임 접근 제한이 그 자체로 매력적이라고 생각하지만, 여기서는 범위상의 이유만으로 이를 제외합니다. PEP 842는 정확히 이를 제안합니다. 코드가 내보내지지 않은 속성에 접근할 때 ExportWarning을 발생시키는 것입니다. 이 논의 스레드에서는 해당 접근이 경고를 발생시켜야 하는지, 예외를 발생시켜야 하는지, 아니면 아무 동작도 하지 않아야 하는지, 그리고 이러한 강제가 정당한 내부 접근과 어떻게 상호 작용해야 하는지에 상당한 노력을 들여 논의했으며, 적대적인 프레이밍, 접근마다 발생하는 성능 오버헤드, 신뢰할 수 없는 경고 필터, pip와 같은 패턴의 파손에 대해 상당한 반발을 불러일으켰습니다. pip에는 공개 API가 전혀 없지만, pip._internal을 의도적으로 임포트하는 pip-tools 같은 도구를 여전히 지원합니다. 이러한 논쟁 중 어느 것도 이 PEP가 해결하는 DRY 문제와는 관련이 없습니다.
내보내기 기록 관리 문제와 “Python이 내부 요소에 대한 접근을 감시해야 하는가”라는 문제는 서로 분리할 수 있습니다. 이 PEP는 전자만 해결하고, 후자는 모듈 경계 관례(서브모듈 앞에 밑줄을 붙이거나 비공개 서브패키지를 사용하는 방식)에 맡깁니다. 이러한 관례는 이미 작동하고 있으며, 해당 스레드에서 논의된 모든 라이브러리에 이미 포함되어 제공됩니다.
나중에 런타임 강제가 필요해진다면, 별도의 제안으로 정확하고 중복되지 않는 __all__ 위에 이를 추가할 수 있으며, 애초에 해당 __all__을 생성하는 구문과 얽히게 만들 필요가 없습니다.
하위 호환성
export는 match, case, type과 동일한 접근 방식을 따르는 소프트 키워드입니다. Python은 import가 다른 경우에 요구되는 유일한 위치, 즉 from <module> 바로 뒤에서만 이를 특별하게 처리합니다. export를 변수, 함수, 매개변수 또는 모듈 이름으로 사용하는 기존 코드는 변경 없이 계속 작동하며, 모듈 이름이 문자 그대로 export인 특이하지만 유효한 경우도 포함됩니다.
from ... export ...는 모든 Python 버전이 이미 이해하는 __all__에만 영향을 줍니다. 이 기능이 도입되기 전의 버전을 지원하는 라이브러리는 두 형식을 모두 작성할 수 있으며, 지원하는 최소 버전이 따라잡으면 이전 형식을 제거할 수 있습니다:
# Python < 3.16
from ._internal.core import PublicAPI
__all__ = ["PublicAPI"]
# Python >= 3.16, once adopted
from ._internal.core export PublicAPI
보안 영향
이 PEP에는 알려진 보안 영향이 없습니다.
이 내용을 가르치는 방법
문서에서는 from <module> export <name>를 독립적인 문장이 아니라 이름이 지정된 레이아웃인 hub-and-internals패턴의 일부로 가르쳐야 합니다. 이 패턴을 따르는 패키지에는 두 종류의 모듈이 있습니다:
- 하나는 일반적으로
__init__.py인 hub모듈입니다(패키지에는numpy.typing처럼 둘 이상이 있을 수 있음). 이 모듈의 유일한 역할은 내부 모듈에서 이름을 모아 다시 내보내는 것입니다. hub 모듈에는export문만 있으며__all__에 영향을 주는 다른 내용은 없습니다.export가 이를 구성하므로__all__을 직접 선언하지도 않습니다. - 나머지는 실제 구현을 담는 임의 개수의 internal모듈입니다. 일반적으로 이름 앞에 밑줄을 붙이거나, 이름 앞에 밑줄이 붙은 서브패키지 안에 중첩하여 이름을 지정합니다. 내부 모듈은
__all__을 선언하지 않습니다. 사용자가 직접 임포트하도록 의도된 모듈이 아니므로__all__이 관리할 대상도 없습니다.
# spam/__init__.py (the hub)
from ._internal.core export PublicAPI
from ._internal.widgets export Widget, Gadget
# spam/_internal/core.py (internal -- no __all__)
class PublicAPI:
...
# spam/_internal/widgets.py (internal -- no __all__)
class Widget:
...
class Gadget:
...
class _Helper:
...
여기서 가르칠 하나의 규칙이 도출됩니다. 이름을 사용자에게 제공해야 한다면 hub에 해당 이름을 위한 ``export``문을 하나 작성하고, 나머지는 모두 기본적으로 내보내지 않은 상태로 둡니다. 전체 패키지는 이름을 실제로 사용할 수 있게 만드는, 어차피 존재했을 문장으로부터 공개 레이아웃을 정확히 한 곳에서 선언합니다.
현재 다시 내보내기를 위해 from <module> import <name> as <name>을 권장하는 스타일 가이드는 대신 export를 가리킬 수 있습니다. export는 조건부 다시 내보내기도 지원하며, 플랫폼별 구현을 선택하는 경우가 그 예입니다(Specification 참조). __all__을 직접 조작하는 방식은 계속 사용할 수 있으며, hub-and-internals 패턴 외부에서는 여전히 필요합니다. 예를 들어 플러그인을 등록하는 루프처럼, 단일 임포트 문이 아니라 런타임에 프로그래밍 방식으로 발견되는 이름의 경우가 이에 해당합니다.
참조 구현
아직 참조 구현은 존재하지 않습니다. CPython의 문법 변경을 확정하기 전에, match문장의 초기 프로토타입과 유사한 소스 대 소스 변환으로 프로토타입을 구축할 수 있습니다.
거부된 아이디어
대체 표면 구문
이 PEP에서는 다시 내보내기 문을 표기하는 두 가지 다른 방식도 검토했습니다:
- ECMAScript의
export ... from ...을 본뜬export <name> from <module>형식입니다. 다른 언어의 관례와 일치시키는 것 외에는 아무런 이점이 없으면서 이름을 모듈보다 앞에 배치하여 모든 Python import 문이 사용하는 순서를 뒤집으므로 거부되었습니다. - 일반적인
from ... import ...문 앞에export를 붙이는export from <module> import <name>형식입니다. 이 제안의 초기 초안에서는 이 형식을 사용했습니다. 완전한 import 문 앞에 선행하는export를 두면 하나의 동작을 두 동사로 읽게 되며,import를 그 자리에서 대체하면 문법에서 키워드의 특수 처리 위치를 한 곳으로 유지할 수 있기 때문에from <module> export <name>형식을 채택하고 이 형식은 거부되었습니다. 그렇지 않으면 파서가 여러 문 종류 앞의 접두사로서export를 인식해야 합니다.
미해결 문제
export는 목록이 아닌 __all__에 대해 경고해야 합니까?
Specification은 목록이 아닌 __all__을 여기에 추가하기 전에 조용히 목록으로 변환합니다. 반면 Guido van Rossum이 제안한 PEP 844의 @public 데코레이터 관련 병렬 제안은 목록이 아닌 __all__을 발견하면 눈에 보이는 DeprecationWarning을 발생시키면서도 해당 값을 무기한 지원합니다 (comment).
미해결 질문은 다음과 같습니다. export도 동일하게 동작하여 목록이 아닌 __all__을 변환해야 할 때 경고해야 합니까, 아니면 조용히 처리하고 ruff의 PLE0605와 같은 린터에 맡겨야 합니까?
감사의 말
이 PEP는 PEP 842에 관한 논의에서 발전했으며, 특히 Peter Bierma, Alex Grönholm, Guido van Rossum, Barry Warsaw, Hugo van Kemenade의 기여가 중요했습니다. 이들은 가상의 사례가 아니라 구체적인 라이브러리를 바탕으로 이 제안을 정립할 수 있도록 실제 환경에서 발생한 재내보내기 손상의 예를 제공했습니다.
각주
변경 이력
- 2026년 8월 23일
__all__정규화 의사 코드를 세 번 반복하는 대신 Specification에 하나의 사본으로 통합했습니다.export가 목록이 아닌__all__에 대해DeprecationWarning을 발생시켜야 하는지 묻는 Open Issues 섹션을 추가했습니다.- DRY의 정식 정의를 인용했습니다.
export를 모듈 수준으로 제한했습니다.
- 2026년 8월 22일
import *와 정확히 일치하도록 와일드카드 형식의 미해결 질문을 해결했으며,__all__이 없는 경우의 대체 동작도 포함했습니다. 이제 해결된 미해결 문제 섹션을 삭제했습니다.- “Interaction with
__all__”에서__all__생성 규칙을 명시적으로 정리하고,export문 뒤에__all__ +=를 사용하는 예를 추가했습니다. export는 자체적인 제한 없이import가 사용되는 모든 곳에서 사용할 수 있음을 명시하고, 스텁 전용 패키지에서if typing.TYPE_CHECKING:를 사용한 재내보내기를 의도된 사용 사례로 언급했습니다.
- 2026년 8월 13일
- “flat tree”가 자기모순적이므로 Abstract의 공개/구현 레이아웃 설명을 다시 작성했습니다.
__all__이 없으면dir()의 깔끔함이 저하된다는 잘못된 주장을 삭제했습니다.dir()은__all__을 참조하지 않습니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.