PEP 842 – 모듈 내보내기
- Author:
- Peter Bierma <peter at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Withdrawn
- Type:
- Standards Track
- Created:
- 25-Jul-2026
- Python-Version:
- 3.16
- Post-History:
- 24-Jul-2026, 31-Jul-2026, 07-Aug-2026
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
Note
이 PEP는 철회되었습니다. 저자가 이 제안을 한 동기는 표준 라이브러리 유지 관리를 개선하는 것이었지만, 이 PEP에서 설명한 해결책은 서드파티 패키지의 요구 사항과 맞지 않았습니다.
초록
이 PEP는 모듈 외부에서 변수가 보이는지에 대한 의도를 모듈이 표현하는 데 사용할 수 있는 export 문을 제안합니다.
예를 들면 다음과 같습니다:
# spam.py
from mypackage export name
export foo = "42"
export class Public:
pass
class Private:
pass
>>> import spam
>>> 'Public' in dir(spam)
True
>>> 'Private' in dir(spam)
False
>>> spam.Public
<class 'spam.Public'>
>>> spam.Private
Traceback (most recent call last):
File "<python-input-4>", line 1, in <module>
spam.Private
ExportError: 'Private' is not exported by 'spam'
이것은 Python의 접근 제어자로 의도된 것이 아닙니다. 근거를 참조하십시오. 이 PEP에서 명시하는 메커니즘은 필요하다면 쉽게 우회할 수 있습니다.
동기
모듈 수준 이름에는 비공개성이 필요합니다
한 개발자가 Python 모듈을 작성하고 있습니다. 이 모듈에는 PublicAPI라는 “공개” 클래스, 즉 모듈 사용자를 위한 클래스가 하나 있도록 의도되어 있습니다. PublicAPI를 구현하는 과정에서 개발자는 Helper라는 다른 클래스를 만들고자 합니다. 그러나 Helper는 PublicAPI가 공개된 것과 같은 방식으로 공개되도록 의도된 것이 아닙니다. Helper는 모듈 개발자만 사용하도록 되어 있는 “비공개” API입니다.
그럼에도 개발자는 두 클래스를 다음과 같이 선언합니다:
# spam.py
class Helper:
...
class PublicAPI:
...
이 방식의 문제는 Helper에 공개 API가 아니라는 표시가 전혀 없다는 점입니다. 이 클래스는 언어 서버의 자동 완성, dir() 함수, Python의 대화형 help() 함수, 그리고 인트로스펙션을 위한 다른 모든 API에 나타납니다. 사용자가 이것을 사용하면 안 된다는 사실을 어떻게 알 수 있겠습니까?
접두사를 붙인 이름이 반드시 훌륭한 해결책은 아닙니다
Python에서 비공개 이름을 선언하는 관례는 이름 앞에 _를 붙이는 것입니다. 그래서 개발자는 Helper를 _Helper로 바꿉니다:
# spam.py
class _Helper:
...
이는 오늘날 Python 라이브러리에서 일반적인 표준이지만, 이것이 장기적으로 최선의 해결책인지는 분명하지 않습니다. 이 방법은 동작하지만(몇 가지 주의 사항이 있습니다. 아래 절들을 참조하십시오), (주관적으로) 가독성이 떨어지고 유지 관리자가 더 많은 키를 입력해야 합니다. 이상적으로는 사용자가 애초에 모듈의 비공개 이름에 손을 대고 싶은 유혹을 느끼지 않아야 합니다.
그러나 이 아이디어가 30년 동안 이어진 관례에 어긋난다는 점은 인정합니다. 이 PEP가 수락되더라도 “밑줄이 붙은” 이름(앞에 _가 붙은 이름)은 앞으로도 수년간 Python의 기본 요소로 남을 것으로 예상됩니다. 이 PEP의 목적은 모듈 수준 이름에서 _의 필요성을 없애는 것이 아니라, 비공개 이름이 모호하거나 사용하고 싶은 유혹을 주는 특수한 경우를 정리하는 것입니다. 다시 말해, 이 PEP는 비공개 API의 표현력과 명확성을 개선하기 위한 것이며, 완전히 새로운 기능을 추가하기 위한 것이 아닙니다.
어디에 접두사를 붙여야 하는지가 항상 분명하지는 않습니다
Python은 여러 가지 구문을 통해 이름을 정의하며, 그중 일부는 개발자에게 항상 명확하거나 직관적이지는 않습니다. 그 결과 어떤 이름에 접두사를 붙여야 하는지 기억하기 어려울 수 있습니다. 이 문제를 구체적으로 이해하기 위해, 개발자가 자신의 코드에서 다른 모듈 몇 개를 임포트하려 한다고 상상해 보십시오:
# spam.py
import argparse
import asyncio
import tabnanny
위 예제에서 spam 모듈은 argparse, asyncio, tabnanny를 겉보기에 공개된 속성으로 갖게 됩니다. 실제로 이는 유지 관리자에게 좋지 않습니다. 유지 관리자는 원하는 대로 임포트를 제거하거나 변경하고 싶을 수 있으므로, 이러한 속성을 공개 API로 취급해서는 안 되기 때문입니다.
Python 표준 라이브러리는 현재 하위 호환성 정책(PEP 387)에 있는 메모를 통해 이 문제를 피해 가고 있습니다. 이 메모는 임포트된 모듈은 공개 API로 간주되지 않으며 언제든지 변경될 수 있다고 명시합니다. 하지만 안타깝게도 사용자는 하위 호환성 정책을 직접 읽지 않고서는 이를 알 수 없으며, 그 정책을 읽는 것은 흔한 일이 아닙니다. 이에 대한 해결책은 임포트하는 모든 이름에도 _ 접두사를 붙이는 것입니다:
# spam.py
import argparse as _argparse
import asyncio as _asyncio
import tabnanny as _tabnanny
하지만 이 방법 역시 코드에 밑줄이 붙은 이름을 더욱 많이 흩뿌리게 되며, 그 이름이 비공개라는 메시지를 반드시 명확하게 전달하는 것도 아닙니다. 다음 절을 참조하십시오.
접두사가 붙은 이름은 보편적인 규칙이 아닙니다
모듈이 발전함에 따라 밑줄이 붙은 일부 이름이 공개되기도 합니다. 사용자가 밑줄이 불안정성을 나타낸다는 것을 명확히 이해하지 못했거나, 모듈의 비공개 API에서 유용한 기능을 발견했는데 그 사용을 막는 것이 아무것도 없었기 때문입니다.
표준 라이브러리에서 이에 대한 대표적인 예는 ctypes 모듈입니다. ctypes는 Python 하위 호환성 정책의 적용을 받는 안정적인 API로 가득하지만, 이 API들은 이름 앞에 밑줄이 붙어 있습니다. 예를 들면 다음과 같습니다:
이는 API 사용자에게 잘못된 메시지를 전달합니다. 코드베이스에서 이런 것을 보면, 해당 코드가 하위 호환성을 포기한 것처럼 보이거나 그 모듈에서는 밑줄이 붙은 이름이 “비공개”를 의미하지 않는 것처럼 보이게 됩니다. 두 경우 모두 사용자는 (그렇게 해도 눈에 띄는 대가가 없으므로) 더 많은 비공개 이름에 손을 뻗게 되고, 이는 문제를 더욱 악화시킵니다.
모듈도 이 문제에서 자유롭지 않습니다. 예를 들어 표준 _thread 모듈은 공개 모듈이면서도 _ 접두사가 붙어 있습니다.
일부 라이브러리에는 네이티브 대응 모듈이 있습니다
경우에 따라서는 임포트에 _ 접두사를 붙이면 모호해집니다. 일부 복잡한 모듈은 네이티브 기능에 대한 접근을 제공하거나 어떤 방식으로든 모듈의 속도를 높이는 확장 모듈을 함께 제공하기 때문입니다. 이러한 네이티브 모듈에는 흔히 앞에 밑줄이 붙습니다.
예를 들어 CPython에서 asyncio 모듈에는 비공개 _asyncio 가속기 모듈이 있으므로, _asyncio를 본 독자는 이를 일반 모듈이 아니라 C 가속기를 의미하는 것으로 받아들일 수 있습니다.
언어 서버와 린터가 임포트를 제안합니다
앞서 설명한 문제로 돌아가 보면, 모듈 수준에서 정의된 임포트는 API 표면에서 “공개” 이름으로 보이게 됩니다. 실제로 모듈을 개발할 때 언어 서버가 제공하는 자동 완성은 해당 모듈이 임포트한 모듈을 임포트하도록 제안하는 경우가 많습니다. 따라서 사용자는 겉보기에 공개된 임포트에 접근하는 것을 제지받지 않을 뿐만 아니라, 언어 서버로부터 그렇게 하도록 권장받을 수도 있습니다! (이 문제는 비공개로 의도된 모든 이름에 적용됩니다. 다만 임포트가 이런 일이 일어나는 특히 흔한 경우일 뿐입니다. 다른 예는 아래를 참조하십시오)
실제 사례
이는 가상의 문제가 아닙니다. 이것이 실제로 문제를 일으킨 사례가 많이 있습니다.
Note
이 목록을 정리해 주신 Hugo van Kemenade 님께 특별히 감사드립니다.
os.errno
Python 3.7에서 os에 있던 errno 모듈 임포트가 제거되었습니다. 이로 인해 많은 문제가 발생했습니다:
requests.packages
requests 패키지에는 사용자들이 API처럼 취급하던 내부 벤더링 네임스페이스가 있었습니다. 그래서 패키지 벤더링을 해제한 뒤에도 requests는 requests.packages를 별칭으로 유지했고, 이 역시 미묘한 문제를 일으켰습니다:
botocore.vendored
botocore 패키지도 botocore.vendored 네임스페이스 아래에 벤더링된 의존성을 두었는데, 결국 사용자들이 이에 의존하게 되었습니다:
SciPy와 pandas
SciPy 패키지와 pandas 패키지 모두 모듈 수준에서 다른 패키지가 노출되어 있었는데, 서드파티에서 이를 사용하고 있었기 때문에 폐기 예정으로 지정한 뒤 제거해야 했습니다:
scikit-learn
scikit-learn 패키지는 six와 joblib을 벤더링했습니다. 그러자 다운스트림 패키지들이 이 벤더링된 사본을 사용했고, v0.23에서 사본이 제거되자 동작하지 않게 되었습니다:
기타 사례
임포트 외에도 사용자가 실수로 내부 API에 접근하여 코드가 깨진 사례가 여러 가지 있습니다.
logging._acquireLock / logging._releaseLock
logging 모듈 문서의 한 예제에 비공개 API가 포함되어 있었습니다. 이후 이 예제가 여러 다운스트림 프로젝트에 그대로 복사되었고, Python 3.13에서 해당 비공개 API가 제거되자 코드가 깨졌습니다:
matplotlib.cbook._check_in_list / matplotlib.cbook._rename_parameter
matplotlib은 일부 유틸리티 함수를 모듈 수준 네임스페이스에 남겨 두었습니다. 이 함수들의 이름은 밑줄로 시작했지만 사용자들이 이를 무시했고, 그 결과 함수들이 제거되자 코드가 깨졌습니다:
concurrent.futures.thread._threads_queues
Python 3.8 시절에는 CTRL+C로 ThreadPoolExecutor를 종료할 수 있게 하는 레시피가 널리 퍼졌습니다. 이 레시피는 내부 API를 사용했지만 많은 사용자가 이를 알아채지 못했고(또는 앞서 설명한 문제 때문에 알아채고도 무시했을 수 있습니다), 그 결과 Python 3.9에서 워커 스레드가 더 이상 데몬 스레드가 아니게 되자 코드가 깨졌습니다:
re._pattern_type
re.Pattern이 도입되기 전에는 re.compile()이 반환하는 객체의 타입이 비공개였습니다. 많은 사용자가 type(re.compile('')) 코드를 쓰는 것보다 내부 타입에 직접 접근하는 편이 더 쉽다고 여겼고, 그 결과 3.7에서 이 타입이 제거되자 코드가 깨졌습니다:
asyncio.staggered_race
aiohappyeyeballs 패키지(aiohttp가 내부적으로 사용합니다)는 asyncio 모듈의 내부 API인 staggered_race를 사용했습니다. 구현이 더 이상 loop 매개변수를 갖지 않도록 변경되면서 이 코드가 깨졌습니다:
린터는 임포트에 맞설 수 없습니다
위 문제의 해결책으로, 린터가 다른 모듈에서 모듈을 임포트하는 것에 그냥 경고를 내면 된다고 제안할 수도 있습니다. 이 방법의 주된 문제는 모든 패키지를 하나의 네임스페이스로 모으기 위해 __init__.py 파일에서 이 패턴이 특히 흔하게 쓰인다는 점입니다. 예를 들면 다음과 같습니다:
# __init__.py
from my_package import subpackage_1
from my_package import subpackage_2
# etc
린터에는 이 패턴을 “일반적인” 임포트와 구별할 언어 수준의 방법이 없습니다. 해결책으로 많은 린터가 의도적인 재공개(re-export)를 식별하기 위해 import name as name을 사용하지만, 이 패턴은 관례일 뿐입니다. 예를 들어, 위의 __init__.py는 다음과 같이 다시 작성됩니다:
# __init__.py
from my_package import subpackage_1 as subpackage_1
from my_package import subpackage_2 as subpackage_2
# etc
이는 중복일 뿐만 아니라(DRY 원칙 위반이기도 합니다) 혼란스럽기까지 합니다! Python 공식 문서는 재노출(re-export)을 위한 이 패턴을 문서화하지 않으므로(언어가 정의한 것이 아니라 린터가 강제하는 관례일 뿐이기 때문입니다), 이 패턴을 사용하는 패키지는 대체로 그저 “아는 사람만 아는” 경우입니다.
하지만 이는 관례일 뿐이므로 린터는 그 반대 경우를 강제할 수 없습니다. 즉, 임포트에 name as name 처리가 되어 있지 않다고 해서 린터가 그 임포트를 재공개가 아니라고 반드시 가정할 수는 없습니다.
우리는 사용자를 외면하지 않고 친절하게 대하고 싶습니다
사용자가 의도했든 아니든 비공개 API를 사용하기로 하면, 언젠가는 라이브러리 작성자 때문에 코드가 깨질 수밖에 없습니다. 이는 많은 경우 다운스트림 코드가 깨지지 않도록 해당 API를 수정하거나 복원해 달라는 버그 보고로 이어집니다. 이 경우 라이브러리 유지 관리자는 결정을 내려야 합니다.
- 사용자에게 그것을 사용한 것이 잘못이라고 말하고, 코드가 깨지도록 내버려 둡니다.
- 비공개 API를 공개 API로 유지 관리하는 데 커밋합니다. 이로 인해 유지 관리자 자신의 부담이 늘어나고 동기에서 설명한 문제 중 일부를 겪게 됩니다.
이 PEP는 이 문제를 완전히 해결하려는 것이 아니라, 사용자가 비공개 이름에 접근하고 있다는 사실을 훨씬 더 분명하게 드러내어 문제를 완화하려는 것입니다. 다시 말해, 이 PEP는 실제로 일어나는 우발적인 비공개 API 사용을 줄이는 것(또는 없애는 것)을 목표로 합니다. 비공개 API에 접근하려면 사용자는 의식적으로 그렇게 하기로 결정해야 합니다.
라이브러리 사용자는 런타임 인트로스펙션을 문서처럼 활용합니다
위 절에 대한 반론은 라이브러리가 무엇이 비공개이고 무엇이 공개인지 명확하게 문서화해야 한다는 것입니다. 이론적으로는 그렇지만, 실제로 사용자는 문서를 처음부터 끝까지 읽지 않습니다.
API를 설계할 때 흔히 따르는 방식은 직관적으로 쓸 수 있게 설계하는 것입니다. API의 이름과 위치가 잘 정해져 있다면, 사용자는 문서를 찾아볼 필요가 없는 경우가 많습니다. Python도 예외는 아닙니다.
프로토타입을 만들 때는 Python의 대화형 REPL에서 dir()이나 help()를 사용해 자신에게 유용한 속성을 찾아보는 것이 일반적입니다. 이런 경우 사용자에게 충분히 직관적인 것이 있으면 사용자는 문서를 먼저 확인하지 않고 곧바로 그것을 가져다 씁니다. Python처럼 동적인 언어에서는 사람들이 API를 사용하는 방식도 동적입니다.
__all__은 관례일 뿐입니다
여기서 근본적인 문제는 Python에는 모듈의 어떤 이름이 “비공개”이고 어떤 이름이 “공개”인지 표현할 방법이 없다는 것입니다. 접두사를 붙이는 것도 한 가지 방법이지만, 위에서 말한 이유 때문에 라이브러리 작성자에게 항상 확실한 해결책이 되지는 않습니다.
현재 어떤 이름이 공개인지 표현하는 또 다른 관례는 모듈의 __all__ 변수를 사용하는 것입니다. 이 방식에는 두 가지 큰 단점이 있습니다:
__all__은 실제 내용과 어긋나는 일이 잦습니다. 개발자가 모듈에 이름을 추가하거나 변경하거나 제거할 때__all__도 함께 바꾸도록 이끄는 장치가 없는 경우가 많기 때문입니다. 다시 말하지만, 공개 이름을 나열하는 데__all__을 사용하는 것은 관례일 뿐이며 어떤 것으로도 강제되지 않습니다.__all__이 항상 모든 공개 이름을 담고 있는 것은 아닙니다.__all__의 항목이 모듈의 “공개” 이름 중 일부에 불과할 수 있는 예시는 기각된 아이디어를 참고하십시오. 요컨대, 네임스페이스 오염을 통제하는 것과 모든 공개 이름을__all__에 선언하는 것을 동시에 해내기는 어려울 수 있습니다.
이 PEP는 새로운 __export__ 변수와 export 문으로 이 두 가지 문제를 모두 해결하고자 합니다.
명세
ExportError 타입
ExportError라는 새로운 예외 타입이 builtins 모듈에 추가됩니다. ExportError는 AttributeError를 상속합니다.
허용되기는 하지만 사용자 코드에서 발생시키도록 의도된 것은 아닙니다. 대신 __export__에 없는 이름에 접근할 때 모듈 객체가 발생시키도록 의도되었습니다. 모듈 속성 접근를 참고하십시오.
C API
Note
이 절은 CPython에만 해당합니다.
ExportError 클래스는 PyExc_ExportError라는 이름으로 공개 C API 헤더에 추가됩니다. 다른 모든 전역 예외 타입과 마찬가지로 안정 ABI에 포함되며, 런타임에 불멸 객체가 됩니다.
__export__ 변수
요구 사항
모듈의 전역 스코프에서 정의할 때 __export__에는 str 객체를 담은 list(또는 그 서브클래스)의 인스턴스를 대입해야 합니다:
__export__ = ["name1", "name2", "name3"]
항목 요구 사항
__export__ 안의 문자열이 모듈에 정의된 이름과 대응할 필요는 없습니다. 다만 정의되지 않은 이름을 포함할 실질적인 이유는 없습니다. 예를 들어, 다음 코드는 유효합니다(즉, 런타임에 예외를 발생시키지 않습니다). 단, 한 가지 주의할 점이 있습니다:
__export__ = ["does not exist"]
주의할 점은 이 코드를 와일드카드 임포트(from module import *)와 함께 사용하면 예외가 발생한다는 것입니다. __all__이 __export__에 의해 암묵적으로 설정되기 때문입니다. 암묵적 __all__ 정의을 참고하십시오.
모듈 속성 접근
모듈의 전역 이름 공간에 __export__가 있으면, 모듈 객체에 있는 속성에 접근할 때마다 해당 속성 이름이 __export__에 있는지도 확인합니다(앞서 명시한 대로 __contains__를 통해 또는 순회를 통해 확인합니다). 속성 이름이 __export__에 없으면 ExportError가 발생합니다. 예를 들면 다음과 같습니다:
# spam.py
a = 42
b = 24
__export__ = ["a"]
>>> import spam
>>> spam.a
42
>>> spam.b
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
spam.b
ExportError: 'b' is not exported by 'spam'
Note
from 임포트도 같은 속성 접근 메커니즘을 사용하므로 이 동작의 영향을 받습니다.
던더 이름
이는 dunder 이름에는 적용되지 않습니다. __dict__나 __file__ 같은 속성은 모듈의 __export__에 포함되어 있지 않더라도 항상 속성 접근을 통해 모듈에서 접근할 수 있습니다. 예를 들면 다음과 같습니다:
# spam.py
__export__ = []
>>> import spam
>>> spam.__name__
'spam'
모듈 __getattr__ 함수
__export__의 동작은 모듈의 __getattr__() 함수로 재정의할 수 없습니다. __getattr__ 함수는 모듈에 정의되지 않은 이름에 대해서만 호출되기 때문입니다. 하지만 모듈 __getattr__이 호출되는 경우에는 __export__가 아무런 효과가 없습니다. 예를 들면 다음과 같습니다:
# spam.py
__export__ = ["exported"]
exported = 42
def __getattr__(name):
if name == "exported":
# This is never triggered!
raise ImportError()
if name == "hello":
# "hello" is never put through the __export__ filter
return 42
raise AttributeError(f"{__name__!r} has no attribute {name!r}")
>>> import spam
>>> spam.__export__
["exported"]
>>> spam.exported
42
>>> spam.hello
42
__dir__ 동작
__export__가 있는 모듈에서는 모듈의 __dir__() 함수가 모듈의 __export__에 없는 이름을 제외하도록 수정됩니다. 속성의 경우와 마찬가지로 이 동작은 dunder 이름에는 적용되지 않습니다. dunder 이름은 __export__에 포함되어 있는지와 관계없이 항상 dir()의 출력에 포함됩니다. 예를 들면 다음과 같습니다:
# spam.py
class Public:
...
class Private:
...
__export__ = ["Public"]
>>> import spam
>>> dir(spam)
['Public', '__builtins__', '__doc__', '__export__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
사용자 정의 모듈 __dir__ 함수
모듈이 자체 __dir__ 메서드를 정의하면 그 메서드가 이 동작보다 우선합니다. __export__에 없는 이름을 제외하는 것은 __dir__ 구현자의 몫입니다. 예를 들면 다음과 같습니다:
# spam.py
a = 42
b = 24
__export__ = ['a']
def __dir__():
return list(globals().keys())
>>> import spam
>>> dir(spam)
[..., 'a', 'b']
암묵적 __all__ 정의
모듈이 __export__를 정의하지만 __all__을 정의하지 않으면, __all__에 __export__가 대입됩니다. 이를 시각화하면 다음과 같습니다:
# spam.py
a = 42
b = 24
c = 'c'
__export__ = ['a', 'b']
# __all__ is implicitly set to ['a', 'b'], so 'c' will not be included in
# wildcard imports.
>>> from spam import *
>>> a
42
>>> b
24
>>> c
Traceback (most recent call last):
File "<python-input-3>", line 1, in <module>
c
NameError: name 'c' is not defined
이는 __export__에 대해 앞서 명시한 요구 사항이 전부가 아니라는 뜻입니다. 이 경우 __export__는 유효한 __all__이기도 해야 하기 때문입니다. 예를 들어, 모듈에 존재하지 않는 이름을 __export__에 포함하면 와일드카드 임포트가 깨집니다:
# spam.py
a = 42
__export__ = ['a', 'noexist']
>>> from spam import *
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
from spam import *
AttributeError: module 'spam' has no attribute 'noexist'
의미론적 구현
모듈에서 __export__를 정의하는 것은 대략 다음 코드를 추가하는 것과 같습니다:
if "__all__" not in globals():
__all__ = __export__
def _is_dunder_name(name):
return (len(name) > 4) and name.startswith("__") and name.endswith("__")
# Attributes not in the __dict__ fall back to the normal lookup
def __getattribute__(name):
try:
value = globals()[name]
except KeyError:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
if _is_dunder_name(name):
return value
if name not in __export__:
raise ExportError(f"{name!r} is not exported by {__name__!r}")
return value
_module = sys.modules[__name__]
# This is a spooky magic function -- pretend it exists for example's sake
patch(_module, '__getattribute__', __getattribute__)
def __dir__():
names = []
for name in globals().keys():
if (name in __export__) or _is_dunder_name(name):
names.append(name)
return names
이름 내보내기
문법
독립 export 문과 export 대입을 허용하도록 문법이 변경됩니다:
export_stmt[stmt_ty]:
| "export" ','.NAME+
| "export" assignment
simple_stmt[stmt_ty] (memo):
| assignment
| &"export" export_stmt
복합 대입(x += y), 서브스크립트(x[y] = z), 속성(x.y = z)은 컴파일 시점에 PEG 액션을 통해 금지된다는 점에 유의하십시오.
독립 내보내기
독립 export 문은 전역 __export__ 리스트에 하나 이상의 이름을 추가하는 축약 표현입니다.
export 문을 사용하면 인터프리터는 먼저 각 이름이 전역 스코프에 존재하는지 확인합니다. 하나라도 존재하지 않으면 NameError가 발생합니다. 그다음 인터프리터는 전역 네임스페이스에 __export__ 변수가 존재하는지 확인합니다. 존재하지 않으면 이 변수에 빈 list 객체가 할당됩니다. 그런 다음 export 문에 사용된 각 이름에 대해, 변수의 이름을 담은 str이 __export__.append 메서드에 첫 번째 위치 인자로 전달됩니다.
예를 들어 설명하면, 다음 코드는:
export NAME1, NAME2
의미상 다음과 동일합니다:
if "NAME1" not in globals():
raise NameError(...)
if "NAME2" not in globals():
raise NameError(...)
try:
__export__
except NameError:
__export__ = []
__export__.append("NAME1")
__export__.append("NAME2")
export 문은 전역 네임스페이스에서만 허용됩니다. 그 밖의 곳(예를 들어 함수 본문 내부)에서 사용하면 컴파일 중에 SyntaxError가 발생합니다.
내보내기 대입
대입문 앞에 export를 붙이면 이름이 정의된 다음 export됩니다.
예를 들어, 다음 코드는:
export NAME1, NAME2 = VALUE1, VALUE2
의미상 다음과 동일합니다:
NAME1 = VALUE1
NAME2 = VALUE2
export NAME1, NAME2
“내보내기 대입” 문은 표준 대입문(a = b, a, b = c, d 등) 및 타입 어노테이션을 포함하는 개별 대입(a: type = b. 반면 단독으로 쓰인 export a: type은 유효하지 않습니다)과 함께 사용할 때 유효합니다. 예를 들어, 다음은 각각 모두 유효합니다:
export hello = "world"
export my, hovercraft = "full of", "eels"
export types_work_too: int = 42
다음은 유효하지 않습니다:
export hello: str
export my: str, hovercraft: str = "full of", "eels"
export name := "walrus"
export hello += "world"
export trees[0] = "the larch"
export something.name = "python"
함수와 클래스 내보내기
문법
export_compound_stmt[stmt_ty]:
| "export" (function_def | class_def)
compound_stmt[stmt_ty]:
| &"export" export_compound_stmt
export 키워드는 class 또는 def 문의 첫 번째 토큰이어야 합니다. def나 class 뒤에 둘 수는 없습니다. 예를 들어, 다음은 유효하지 않습니다:
async def export name(): # NOT VALID
...
class export Name: # NOT VALID
...
동작
함수 정의문이나 클래스 정의문 앞에 export를 붙여 이름을 자동으로 내보낼 수 있습니다.
예를 들어 설명하면, 다음 코드는:
export def NAME1():
...
export class NAME2:
...
의미상 다음과 동일합니다:
def NAME1():
...
export NAME1
class NAME2:
...
export NAME2
대입 및 단독 내보내기와 마찬가지로, 전역 스코프 밖에서 export def나 export class를 사용하면 컴파일 시점에 SyntaxError가 발생합니다.
그 밖의 주의 사항은 없습니다. 클래스와 함수의 다른 모든 문법 기능은 export를 앞에 붙여도 그대로 동작합니다.
모듈 재내보내기 문
문법
새 규칙이 추가되고 기존 import_from 규칙이 수정됩니다:
import_or_export[expr_ty]:
| 'import'
| "export"
import_from[stmt_ty]:
| "lazy"? 'from' ('.' | '...')* dotted_name import_or_export import_from_targets
| "lazy"? 'from' ('.' | '...')+ import_or_export import_from_targets
동작
“모듈 재내보내기 문”은 from 임포트의 동작을 확장한 것입니다. 이 문은 from 임포트와 완전히 동일한 일을 하지만, 임포트한 각 이름을 export하기도 합니다.
예를 들어, 다음 코드는:
from MODULE export NAME1, NAME2
의미상 다음과 동일합니다:
from MODULE import NAME1, NAME2
export NAME1, NAME2
다른 export 구문과 마찬가지로, 이 구문은 모듈 수준에 있어야 합니다. 다른 곳에서 사용하면 SyntaxError가 발생합니다.
PEP 810에서 설명하는 지연 임포트도 from 내보내기와 함께 사용할 수 있습니다. 예를 들면 다음과 같습니다:
lazy from foo export bar
지연 임포트에 대한 기존 규칙이 여기에도 똑같이 적용됩니다.
근거
이것은 접근 제어자가 아닙니다
이 PEP는 모듈의 비공개 속성에 대한 접근을 막는 메커니즘을 목표로 하지 않습니다. ExportError는 우회하거나 피할 수 있습니다. 아래를 참조하십시오.
이는 의도된 설계입니다. Python이 접근 제한자를 언어 기능으로 포함하지 않는 데에는 이유가 있습니다. Eric Smith의 말을 인용하자면 “필요할 때에는 다른 클래스의 내부에 접근할 수 있다는 것이 하나의 기능입니다”. 이 PEP는 이 관례를 바꾸려는 것이 아니며, Python이 진정한 접근 제한자를 도입하는 방향으로 나아가고 있다는 신호로 해석해서도 안 됩니다.
이 PEP의 의도는 런타임에 모듈을 살펴볼 때의 명확성을 높이는 것입니다. 이는 결과적으로 장기적으로 Python 모듈 유지 관리자의 경험을 개선할 것입니다.
하위 호환성
기존 코드를 변경할 필요가 없습니다
이 PEP에서 설명하는 기능은 모듈이 전역 스코프에 __export__를 정의할 때(또는 __export__를 암묵적으로 정의하는 export 문을 사용할 때)에만 활성화됩니다. 그렇게 하지 않는 모듈은 모든 이름이 기본적으로 내보내지는 현재 동작을 그대로 따릅니다.
__export__ 오버로드
이 PEP는 이미 __export__라는 전역 변수를 정의하고 있던 사용자의 코드를 깨뜨릴 가능성이 있습니다. 그렇지만 Python 언어 레퍼런스는 애초에 사용자가 이렇게 하는 것을 명시적으로 금지합니다.
export (소프트) 키워드
이 PEP에서 제안하는 export는 소프트 키워드입니다. 이는 하위 호환성을 깨뜨리지 않습니다. 즉, “export”를 변수 이름으로 사용하는 기존 코드는 계속 동작합니다.
보안 영향
이 PEP에는 알려진 보안 영향이 없습니다.
교육 방법
export 문과 __export__ 변수는 모두 언어 표준의 일부로 문서화될 것입니다.
하위 호환되는 코드베이스 유지하기
도입을 돕기 위해, 사용자가 모듈에 __all__과 __export__를 모두 정의하도록 권장할 것입니다. 이렇게 하면 Python 3.16 이상의 코드는 올바른 export 동작을 얻으면서도, 이전 버전에서는 __all__ 속성이 그대로 유지됩니다. 실제로는 대략 다음과 같은 모습이 될 것입니다:
__all__ = ["hovercraft"]
__export__ = __all__ + ["eels"]
또는 패키지의 __all__이 __export__와 동등하다면 다음과 같습니다:
__export__ = __all__
JavaScript의 export 키워드와의 비교
JavaScript에서는 이름이 기본적으로 비공개이므로 export 키워드는 “이 이름을 공개로 만들기”라는 의미입니다. 이 개념은 Python에 잘 맞지 않습니다. Python에서는 모듈의 네임스페이스에 __export__가 있는 경우를 제외하고는 이름이 기본적으로 공개(즉, 임포트 가능)되기 때문입니다. 따라서 Python에서 export를 더 명확하게 정의하면 “이 이름을 제외한 나머지를 모두 비공개로 만든다”입니다.
__export__ 우회하기
앞서 언급했듯이 이 제안은 비공개 변수를 철벽같이 보호하기 위한 것이 아닙니다.
프로토타이핑 용도라면 __export__를 우회하는 가장 간단한 방법은 그냥 삭제하는 것입니다:
import module
del module.__export__
# All private variables in 'module' are now available
또는 더 세밀하게 우회하려면 특정 비공개 이름을 __export__에 추가하십시오:
import module
module.__export__.append("name_you_want")
하지만 이 방법은 __export__ 리스트를 전역적으로 수정하므로 다른 패키지 내부의 강제 적용도 비활성화됩니다. 이를 피하려면 모듈의 __dict__를 통해 비공개 변수에 접근하십시오:
import module
name_you_want = module.__dict__["name_you_want"]
참조 구현
이 PEP의 참조 구현은 여기에서 찾을 수 있습니다.
성능
참조 구현은 현재 __export__ 조회나 순회의 오버헤드를 줄이기 위한 최적화를 전혀 구현하지 않았으므로 어느 정도 오버헤드가 있을 가능성이 높습니다. 하지만 이 PEP가 수락되면 이 기능이 CPython에 반영되기 전에 최적화가 구현될 것입니다.
기각된 아이디어
익스포트에 __all__ 재사용하기
새로운 __export__ 변수를 추가하는 대신 이름 지정에 __all__을 재사용하는 대안이 있었습니다.
이름이 __export__에는 있지만 __all__에는 없는 경우가 분명히 존재한다고 판단되었기 때문에 결국 이 방안은 채택되지 않았습니다. 이러한 경우의 대표적인 예는 정적 타이핑이었습니다. 예를 들어 어떤 모듈은 와일드카드 임포트로 사용하면 네임스페이스를 오염시킬 수 있는 여러 타입 별칭을 정의할 수 있으므로 개발자는 이를 __all__에 포함하지 않기로 합니다. 하지만 정적 타이핑 사용자는 자신의 코드에 어노테이션을 달기 위해 여전히 이러한 타입 별칭에 접근하려 할 것입니다.
또한 이 동작을 모든 경우에 걸쳐 표현할 수 있는 좋은 표기법이 있는지도 분명하지 않습니다. “당연한” 해결책은 __all__을 더 엄격하게 만드는 새로운 future 문을 추가하는 것이지만, 이는 하위 호환성이 없습니다. 이 PEP에서 설명하는 동작을 선택하려는 코드베이스는 이전 버전에서도 코드가 계속 동작하도록 지원 대상인 모든 파이썬 버전에서 동작하는 표기법을 사용해야 합니다. 따라서 __all__에 특별한 기능을 추가하는 해결책은 대체로 효과가 없습니다.
노출되지 않은 속성에 접근할 때 경고 발생
이 PEP는 처음에 __export__에 나열되지 않은 모듈 속성에 접근하면 ImportError를 발생시키자고 제안했습니다. PEP가 제안의 의도를 명확하게 설명하지 못했기 때문에 이 방안은 좋은 반응을 얻지 못했습니다. 그 피드백에 따라 ImportError는 경고로 바뀌었습니다. 하지만 경고는 예외보다 사용성이 훨씬 나쁘고, 많은 테스트 프레임워크(pytest 등)가 테스트 중에 경고를 예외로 바꾸기 때문에 결국 나쁜 절충안으로 판단되었습니다.
__export__만 단독으로 도입하기
이 제안의 최초 개정판은 __export__를 독립적인 변수로 포함했으며 새로운 문법은 제공하지 않았습니다. 이 방안의 장점은 하위 호환성이 있다는 것이었습니다. 프로젝트는 __export__ = __all__이라고 작성하기만 하면 되었고, 이후 사용자가 __export__를 지원하는 버전으로 업그레이드하면 이 PEP에서 설명하는 문서화 및 강제 적용의 이점을 얻게 됩니다.
결국 이 방안은 너무 보수적이라고 결정되었습니다. __export__는 __all__과 호환되었지만, 리스트에 항목을 추가하거나 제거하는 것을 잊는 등 __all__의 여러 문제를 그대로 공유했기 때문입니다.
Guido van Rossum의 말을 인용하면 다음과 같습니다:
하지만 사용성은__all__과 비슷하며, 그 사용성은 좋지 않습니다. 목록에 무언가를 추가하는 것(또는 제거하는 것!)을 잊기가 너무 쉽고, 내보내는 대상의 정의와는 완전히 다른 파일 위치에서 내보내기 정보를 갱신해야 하는 것은 주의를 흐트러뜨립니다.
클래스 본문을 위한 private 키워드 추가
이 제안을 논의하는 과정에서, 클래스에서 사용할 private 키워드를 추가하자는(또는 클래스 본문에서 export를 허용하자는) 제안이 있었습니다. 예를 들면 다음과 같습니다.
class Something:
private def hello(self):
print("Hello, world!")
export def goodbye(self):
print("Goodbye, world!")
이는 이 PEP의 범위를 벗어나는 것으로 간주되며, 향후 제안에서 다시 검토될 수 있습니다.
public과 private 데코레이터를 내장 기능으로 추가
새로운 export 키워드를 추가하는 대신, Barry Warsaw의 atpublic 패키지를 바탕으로 한 private과 public 데코레이터를 builtins 모듈에 추가하자는 제안이 있었습니다.
이 데코레이터들은 새로운 문법 없이도 이 PEP와 동일한 문서화 측면을 제공하고, 잠재적으로는 동일한 강제 측면도 제공했을 것입니다. 예를 들면 다음과 같습니다.
@public
class MyPublicClass:
...
# Or
@private
class MyPrivateClass:
...
이는 저자가 export 문법 다음으로 선호하는 해결책이지만, 몇 가지 주의할 점이 있습니다. 특히 이름을 중복하지 않고는 단순한 변수를 내보낼 쉬운 방법이 없는데, 많은 사람이 이를 DRY 원칙 위반이라는 이유로 싫어합니다.
그럼에도 불구하고, 이는 경쟁 제안인 PEP 844에 명시되어 있습니다.
모듈 이름을 기본적으로 공개로 하기
Python에서는 이름이 기본적으로 공개되므로 이 제안의 현재 동작이 직관에 어긋난다고 주장하는 사람이 많았습니다. 이는 다른 언어와 비교했을 때 export의 의미가 성립하지 않는다는 뜻입니다(자바스크립트가 이를 처리하는 방식을 참조하십시오). 해결책으로, 이름을 기본적으로 공개로 유지하고 어떤 이름이 비공개인지 기술하기 위해 private 키워드(__private__ 목록과 함께)를 추가하자는 제안이 있었습니다. 예를 들면 다음과 같습니다.
def public_name():
...
private def private_name():
...
이는 이 제안이 의도한 목표(모듈에서 어떤 이름이 공개인지 더 명확하게 하는 것)를 실제로 달성하기 훨씬 어렵게 만들기 때문에 거부되었습니다.
이 제안의 목적은 비공개 이름이 공개 네임스페이스로 새어 나가는 것을 막는 것이지만, 제안된 __private__ 기능을 사용하면 개발자는 정의될 수 있는 모든 이름을 알고 고려해야 합니다. export에는 이러한 문제가 없습니다. 개발자는 공개되어야 하는 몇 안 되는 이름만 고려하면 되고, 그 이후로는 비공개 여부에 대해 다시 생각할 필요가 없습니다. __export__ 대신 __private__를 사용한다면 모듈 작성자들이 이름을 private로 표시하는 것을 자주 잊을 것이고, 그 결과 사용자들은 비공개 이름을 공개 이름으로 오해하게 될 것으로 예상됩니다.
또한 대규모 모듈은 임포트, 헬퍼 함수 등으로 인해 공개 이름보다 비공개 이름이 훨씬 더 많은 경향이 있으므로, 개발자에게 기계적으로도 더 어렵습니다.
마지막으로, export가 이해하기 어렵다고는 생각하지 않습니다. 앞서 언급했듯이 Python의 export는 JavaScript의 export 정의와 일치하지 않습니다. 하지만 그 점을 제외하면, export가 무엇을 하는지, 그리고 왜 그렇게 하는지는 Python 사용자가 충분히 이해할 수 있는 것으로 보입니다.
미해결 문제
패키지는 자신의 비공개 멤버에 어떻게 접근해야 합니까?
어떤 패키지에 다음 두 모듈이 있다고 가정해 보십시오.
library/utils.py:library의 개발자만을 위한 유틸리티를 담도록 의도된 모듈입니다.library/main.py:library의 사용자가 사용할 수 있는 공개 API를 담고 있는 모듈입니다.
utils.py의 이름들은 내보내지지 않는데, 이 모듈은 library의 사용자가 접근하도록 의도된 것이 아니기 때문입니다. 하지만 main.py는 이 이름들에 접근할 수 있어야 합니다. 현재 제안대로라면 main.py가 utils.py에서 비공개 이름을 임포트할 때 ExportError가 발생하게 됩니다.
이 문제는 어떻게 해결해야 합니까? 애초에 이것이 필요하기는 합니까? 즉, utils.py가 자신의 유틸리티를 내보낸 것으로 표시하고, 사용자에게는 그 안의 어떤 것도 임포트하지 말도록 요청해야 합니까?
export에 최상위 표시자가 필요합니까?
export의 사용은 모듈에 정의된 다른 모든 이름의 런타임 동작에 영향을 미치므로, 경우에 따라서는 유지 보수를 더 어렵게 만들 수 있다는 주장이 있었습니다. 예를 들어, 개발자가 어떤 모듈이 이미 export를 사용하는지 확신하지 못한다면, 나머지 코드에 영향을 주지 않고 공개 API를 export로 선언해도 안전한지 알기 위해 모듈에서 이를 검색해야 합니다.
해결책으로 export 문법을 사용하려면 모듈 최상단에 일종의 표시(__export__ = [] 선언이나 __future__ 임포트 등)를 두도록 요구하자는 제안이 나왔습니다. 위에서 설명한 문제가 실제로 문제가 될지 불분명하기 때문에 이 사항은 아직 결정되지 않았습니다. 많은 라이브러리가 내부적으로 export/__export__를 일관되게 사용할 것으로 예상되며, 따라서 개발자가 자신이 작업 중인 모듈이 어떤 종류인지 파악하는 것은 그리 어렵지 않을 것입니다.
감사의 글
이 PEP의 아이디어에 영감을 준 Hugo van Kemenade와 Savannah Ostrowski에게 감사드립니다.
또한, 이 PEP의 설계는 Guido van Rossum, Paul Moore, Steve Dower, Barry Warsaw를 비롯한 많은 사람의 논의와 아이디어에 크게 영향을 받았습니다.
변경 이력
- 2026년 8월 15일
- PEP를 철회했습니다.
- 2026년 8월 12일
export문이 서브스크립트 및 속성 대입과 함께 동작하는지를 명확히 했습니다.- 파일 최상단에
export문법이 필요한지에 대한 미해결 문제를 추가했습니다. - 실제 사례에 대한 예제를 더 추가했습니다.
- “How To Teach This”에
__export__를 우회하는 방법에 관한 절을 추가했습니다.
- 2026년 8월 11일
__export__가 항상list객체여야 하도록 요구했습니다.- 논의 스레드에서 나온 질문을 바탕으로 PEP의 일부 내용을 명확히 했습니다.
ExportError내장 타입을 추가하고(ExportWarning은 제거), 내보내지 않은 이름에 접근할 때 다시 예외를 발생시키도록 변경했습니다.
- 2026년 8월 5일
export문을 추가했습니다.ExportWarning내장 타입을 추가했습니다. 이제 노출되지 않은 속성에 접근하면RuntimeWarning대신 이 경고가 발생합니다.
- 2026년 8월 1일
- 내보내지 않은 속성에 접근하면 이제
ImportError를 발생시키는 대신RuntimeWarning을 발생시킵니다. - 동기 절을 대폭 개편했습니다.
- 내보내지 않은 속성에 접근하면 이제
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.