Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 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 모듈을 작성하고 있습니다. 이 모듈에는 모듈 사용자를 위한 클래스인 하나의 “public” 클래스가 있어야 하며, 그 이름은 PublicAPI라고 합니다. 개발자는 PublicAPI를 구현하는 과정에서 Helper라는 다른 클래스를 만들려고 합니다. 그러나 HelperPublicAPI와 같은 방식으로 public이어서는 안 됩니다. Helper는 모듈 개발자만 사용해야 하는 “private” API입니다.

그럼에도 개발자는 다음과 같이 두 클래스를 선언합니다.

# spam.py
class Helper:
   ...

class PublicAPI:
   ...

이렇게 하면 Helper에는 이것이 public API가 아니라는 표시가 전혀 없습니다. 언어 서버의 자동 완성, dir() 함수, Python의 대화형 help() 함수 및 인트로스펙션을 위한 그 밖의 모든 API에 나타납니다. 사용자는 이것을 사용해서는 안 된다는 사실을 어떻게 알아야 합니까?

접두사가 붙은 이름이 반드시 좋은 해결책은 아닙니다.

Python에서는 private 이름을 선언할 때 이름 앞에 _를 붙이는 것이 관례입니다. 따라서 개발자는 Helper_Helper로 변경합니다.

# spam.py
class _Helper:
   ...

이는 오늘날 Python 라이브러리의 일반적인 표준이지만, 이것이 장기적으로 최선의 해결책인지는 분명하지 않습니다. 이 방식은 작동하지만(몇 가지 주의 사항이 있으며 아래 절을 참조하십시오), 주관적으로 가독성이 떨어지고 유지 관리자가 더 많은 키 입력을 해야 합니다. 이상적으로는 사용자가 애초에 모듈의 private 이름을 사용하려고 하지 않아야 합니다.

그러나 이 아이디어가 30년간의 관례에 어긋난다는 점은 인정합니다. 이 PEP가 수용되더라도 “밑줄이 붙은” 이름(앞에 _가 붙은 이름)은 앞으로도 수년간 Python의 중요한 요소로 남을 것으로 예상됩니다. 이 PEP의 목적은 모듈 수준 이름에서 _의 필요성을 없애는 것이 아니라, private 이름이 모호하거나 사용하고 싶게 만드는 특수한 경우를 명확히 하는 것입니다. 다시 말해, 이 PEP는 private 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도 포함하고 있습니다. 예를 들면 다음과 같습니다.

  1. ctypes._CFuncPtr
  2. ctypes._CData
  3. ctypes._Pointer

이는 API 사용자에게 잘못된 메시지를 전달합니다. 코드베이스에서 이런 것을 보면 해당 코드가 하위 호환성을 포기하는 것처럼 보이거나, 모듈에서 밑줄이 붙은 이름이 “비공개”를 의미하지 않는 것처럼 보입니다. 두 경우 모두 사용자는 더 비공개인 이름을 사용하려는 경향을 보입니다. 그렇게 해도 겉으로 드러나는 결과가 없기 때문이며, 이는 문제를 더욱 악화시킵니다.

모듈도 이 문제에서 자유롭지 않습니다. 예를 들어 표준 _thread 모듈은 공개 모듈이지만 _가 접두사로 붙어 있습니다.

일부 라이브러리에는 네이티브 대응 모듈이 있습니다

어떤 경우에는 임포트 앞에 _를 붙이면 모호해집니다. 일부 복잡한 모듈에는 네이티브 기능에 대한 접근을 제공하거나 어떤 방식으로든 모듈의 속도를 높이는 확장 모듈이 함께 제공되기 때문입니다. 이러한 네이티브 모듈에는 앞에 밑줄이 접두사로 붙는 경우가 많습니다.

예를 들어 CPython에서 asyncio 모듈에는 비공개 _asyncio 가속기 모듈이 있으므로, _asyncio를 본 독자는 이를 일반 모듈이 아니라 C 가속기라고 이해할 수 있습니다.

언어 서버와 린터가 임포트를 제안합니다

앞서 설명한 문제로 돌아가면, 모듈 수준에서 정의된 임포트는 API 표면에서 “공개” 이름으로 표시됩니다. 실제로 모듈을 개발할 때 언어 서버가 제공하는 자동 완성은 해당 모듈이 임포트한 모듈을 임포트하라고 제안하는 경우가 많습니다. 따라서 사용자는 겉보기에는 공개된 임포트에 접근하지 못하도록 방지되지 않을 뿐 아니라, 언어 서버에 의해 그렇게 하도록 권장될 수도 있습니다! (이 문제는 비공개로 사용하려는 모든 이름에 적용됩니다. 다만 임포트가 이런 일이 발생하는 특히 흔한 경우일 뿐입니다. 다른 예시는 아래를 참조하십시오.)

실제 사례

이는 가상의 문제가 아닙니다. 실제로 이로 인해 문제가 발생한 사례는 많습니다.

Note

이 목록을 compiling this list작성해 주신 Hugo van Kemenade님께 특별히 감사드립니다.

os.errno

Python 3.7에서 errno 모듈을 os에서 가져오는 임포트가 제거되었습니다. 이로 인해 많은 문제가 발생했습니다:

requests.packages

requests 패키지에는 사용자가 API로 취급한 내부 벤더링 네임스페이스가 있었으므로, 패키지의 벤더링을 해제한 후에도 requestsrequests.packages를 별칭으로 유지했으며, 이로 인해 자체적으로 미묘한 문제가 발생했습니다:

botocore.vendored

botocore 패키지에도 botocore.vendored 네임스페이스 아래에 벤더링된 의존성이 있었으며, 결국 사용자가 의존하게 되었습니다:

SciPy 및 pandas

SciPypandas 패키지 모두 모듈 수준에서 다른 패키지를 노출했으며, 서드파티 사용으로 인해 해당 기능을 더 이상 사용하지 않도록 하고 제거해야 했습니다:

scikit-learn

scikit-learn 패키지는 sixjoblib를 벤더링했습니다. 이후 다운스트림 패키지에서 해당 벤더링된 사본을 사용했으며, v0.23에서 이것들이 제거되자 문제가 발생했습니다:

기타 예시

임포트를 넘어서는 경우에도 사용자가 내부 API에 실수로 접근하여 문제가 발생한 사례가 여러 가지 있습니다.

logging._acquireLock / logging._releaseLock

logging 모듈의 문서에는 예제에 비공개 API가 포함되어 있습니다. 이후 이 예시는 여러 downstream 프로젝트에 복사되어 사용되었으며, Python 3.13에서 비공개 API가 제거되자 작동하지 않게 되었습니다.

matplotlib.cbook._check_in_list / matplotlib.cbook._rename_parameter

matplotlib은 일부 유틸리티 함수를 모듈 수준 네임스페이스에 남겨 두었습니다. 이러한 함수 이름에는 앞에 밑줄이 붙어 있었지만 사용자는 이를 무시했고, 함수가 제거되자 문제가 발생했습니다.

concurrent.futures.thread._threads_queues

Python 3.8에서는 ThreadPoolExecutor를 CTRL+C로 종료할 수 있게 하는 방법이 널리 퍼졌습니다. 이 방법은 내부 API를 사용했으며, 많은 사용자가 이를 알지 못했습니다(또는 알고도 앞서 설명한 문제 때문에 무시했을 가능성이 있습니다). 그 결과 Python 3.9에서 작업자 스레드가 더 이상 데몬 스레드가 아니게 되자 문제가 발생했습니다.

re._pattern_type

re.Pattern의 존재 이전에는 re.compile이 반환하는 객체의 타입이 비공개였습니다. 많은 사용자는 type(re.compile(''))를 사용하는 것보다 내부 타입에 접근하는 편이 더 쉽다고 생각했으며, 이 타입이 3.7에서 제거되자 문제가 발생했습니다.

asyncio.staggered_race

aiohappyeyeballs 패키지는 (내부적으로 aiohttp가 사용하는 패키지로서) asyncio 모듈의 내부 staggered_race API를 사용했습니다. 구현이 더 이상 loop 매개변수를 갖지 않도록 업데이트되면서 이 문제가 발생했습니다:

린터는 임포트에 맞설 수 없습니다

위 문제의 해결책으로, 린터가 다른 모듈에서 모듈을 임포트하는 것을 단순히 경고해야 한다고 제안할 수도 있습니다. 이 방식의 가장 큰 문제는 모든 패키지를 하나의 네임스페이스로 옮기기 위해 __init__.py 파일에서 특히 흔히 사용된다는 점입니다. 예를 들면 다음과 같습니다:

# __init__.py

from my_package import subpackage_1
from my_package import subpackage_2
# etc

린터에는 이 방식을 “표준” 임포트와 구별할 언어 수준의 방법이 없습니다. 해결책으로 많은 린터는 의도적인 재내보내기를 식별하기 위해 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 principle를 위반하기도 하므로 혼란스럽습니다! Python 공식 문서는 재내보내기에 이 방식을 사용하라고 문서화하지 않습니다(언어로 정의된 것이 아니며 린터가 강제하는 관례일 뿐이기 때문입니다). 따라서 이렇게 하는 패키지는 주로 “아는 사람들”만 사용하는 패키지입니다.

그러나 이것은 관례일 뿐이므로 린터는 부정적인 경우를 강제할 수 없습니다. 임포트에 name as name 처리가 적용되지 않았다고 해서 린터가 반드시 해당 임포트가 재내보내기가 아니라고 가정할 수는 없습니다.

사용자를 외면하지 말고 친절하게 대해야 합니다

사용자가 의도했든 그렇지 않든 비공개 API를 사용하기로 결정하면, 라이브러리 작성자에 의해 결국 문제가 발생합니다. 많은 경우 이로 인해 하위 호환성 문제를 방지하기 위해 API를 수정하거나 복원해 달라는 버그 보고서가 제출됩니다. 이 경우 라이브러리 유지 관리자는 결정을 내려야 합니다:

  1. 사용자가 그것을 사용한 것이 잘못이라고 말하고, 문제가 발생하도록 내버려 둡니다.
  2. 비공개 API를 공개 API로 유지하겠다고 약속하여 자신의 부담을 늘리고 the motivation에 설명된 일부 문제에 직면합니다.

이 PEP는 이 문제를 완전히 해결하려는 것이 아니라, 사용자가 비공개 이름에 접근하고 있다는 사실을 훨씬 더 명확하게 하여 문제를 완화하려는 것입니다. 다시 말해, 이 PEP는 실제로 실수로 비공개 API를 사용하는 양을 줄이거나 없애고자 합니다. 비공개 API에 접근하려면 사용자는 그렇게 하겠다는 의식적인 결정을 내려야 합니다.

라이브러리 사용자는 문서화를 위해 런타임 인트로스펙션을 사용합니다

위 절에 대한 반론은 라이브러리가 무엇이 비공개이고 무엇이 공개인지 명확하게 문서화해야 한다는 것입니다. 이론적으로는 그렇지만, 실제로 사용자는 문서를 처음부터 끝까지 읽지 않습니다.

API를 설계할 때 흔히 사용하는 방식은 직관적으로 설계하는 것입니다. API의 이름이 적절하고 위치가 잘 정해져 있다면, 사용자는 문서를 찾아보지 않아도 되는 경우가 많습니다. Python도 예외가 아닙니다.

프로토타이핑할 때는 Python의 대화형 REPL에서 자신에게 유용한 속성을 찾기 위해 dir() 또는 help()를 사용하는 것이 일반적입니다. 이 경우 사용자에게 무언가가 충분히 직관적이라면, 사용자는 먼저 문서를 확인하지 않고도 그것을 바로 사용합니다. Python처럼 동적인 언어에서는 사람들이 API를 사용하는 방식 또한 동적입니다.

__all__은 단지 관례일 뿐입니다.

여기서 근본적인 문제는 Python에는 모듈의 어떤 이름이 “비공개”이고 어떤 이름이 “공개”인지 표현할 방법이 없다는 점입니다. 접두사를 사용하는 방법도 있지만, 위에서 설명한 이유로 라이브러리 작성자에게 항상 완벽한 해결책인 것은 아닙니다.

현재 공개 이름을 표현하는 또 다른 관례는 모듈의 __all__ 변수를 통해 이루어집니다. 여기에는 두 가지 주요 단점이 있습니다.

  1. 개발자가 모듈에 이름을 추가하거나 변경하거나 제거할 때 __all__을 변경하도록 유도하는 것이 대개 아무것도 없기 때문에 __all__은 자주 최신 상태와 어긋납니다. 다시 말해 공개 이름을 나열하는 데 이를 사용하는 것은 단지 관례일 뿐이며 어떤 것에 의해서도 강제되지 않습니다.
  2. __all__은 항상 모든 항목을 포함하는 것은 아닙니다. __all__의 항목이 모듈의 “공개” 이름 중 일부에 불과할 수 있는 사례는 rejected ideas를 참조하십시오. 요컨대 네임스페이스 오염을 제어하면서 동시에 모든 공개 이름을 __all__에 선언하기는 어려울 수 있습니다.

이 PEP는 새로운 __export__ 변수와 export 문을 통해 이 두 문제를 모두 해결하려고 합니다.

사양

ExportError 형식

ExportError라는 새로운 예외 형식이 builtins모듈에 추가됩니다. ExportErrorAttributeError를 상속합니다.

허용되기는 하지만 사용자 코드가 이를 발생시키도록 의도된 것은 아닙니다. 대신 __export__에 없는 이름에 액세스할 때 module객체가 이를 발생시키도록 의도되었습니다. 모듈 속성 액세스를 참조하십시오.

C API

Note

이 절은 CPython에만 해당합니다.

ExportError 클래스가 PyExc_ExportError라는 이름으로 공개 C API 헤더에 추가됩니다. 다른 모든 전역 예외 형식과 마찬가지로 Stable ABI에 포함되며 런타임에는 immortal입니다.

__export__ 변수

요구 사항

모듈의 전역 범위에 정의할 때 __export__list의 인스턴스(또는 그 서브클래스)이며 str객체를 포함하도록 할당해야 합니다.

__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__에 없는 이름을 제외하도록 수정됩니다. 속성과 마찬가지로 이 동작은 듀더 이름에는 적용되지 않습니다. 듀더 이름은 __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__ 메서드를 정의하면 이 동작보다 우선합니다. __dir__ 구현자가 __export__에 없는 이름을 제외해야 합니다. 예를 들어 다음과 같습니다.

# 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__에 대한 previously specified requirements이 완전하지 않다는 의미입니다. 이 경우 __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합니다.

예를 들어, 다음 코드는:

export NAME1, NAME2 = VALUE1, VALUE2

의미상 다음과 같습니다:

NAME1 = VALUE1
NAME2 = VALUE2
export NAME1, NAME2

“Export 할당” 문은 표준 할당 문(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 가져오기의 동작을 확장한 것으로, 정확히 동일하게 동작하지만 가져온 각 이름에도 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 이상용 코드는 올바른 내보내기 동작을 얻는 반면, 이전 버전은 여전히 해당 __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에 도입되기 전에 최적화가 구현될 것입니다.

거부된 아이디어

export에 __all__를 재사용

새로운 __export__ 변수를 추가하는 대신 이름에 __all__를 재사용하는 대안이 있었습니다.

결국 이름이 __export__에는 포함될 수 있지만 __all__에는 포함되지 않는 경우가 분명히 존재하는 것으로 판단되어 이 방안은 채택되지 않았습니다. 이 경우의 주요 예는 정적 타이핑이었습니다. 예를 들어 모듈에서 여러 타입 별칭을 정의할 수 있는데, 이를 와일드카드 임포트와 함께 사용하면 네임스페이스를 오염시킬 수 있으므로 개발자는 이를 __all__에 포함하지 않기로 선택할 수 있습니다. 그러나 정적 타이핑 사용자는 자신의 코드를 어노테이션하기 위해 이러한 타입 별칭에 계속 접근하기를 원할 것입니다.

또한 모든 경우를 포괄하면서 이 동작을 표현할 적절한 표기법이 있는지도 분명하지 않습니다. “명백한” 해결책은 __all__를 더 엄격하게 만드는 새로운 future 문을 추가하는 것이지만, 이는 하위 호환되지 않습니다. 이 PEP에서 설명하는 동작을 선택하려는 코드베이스는 이전 버전에서도 코드가 작동하도록 지원되는 모든 Python 버전에서 동작하는 표기법을 사용해야 하므로, 일반적으로 __all__에 특수 기능을 추가하는 해결책은 작동하지 않습니다.

export되지 않은 속성에 접근할 때 경고 발생

이 PEP는 처음에 __export__에 나열되지 않은 모듈 속성에 접근할 때 ImportError를 발생시키도록 제안했습니다. 그러나 PEP에서 이 제안의 의도를 명확히 설명하지 않았기 때문에 좋은 반응을 얻지 못했습니다. 이러한 피드백에 따라 ImportError는 경고로 바뀌었지만, 결국 이는 좋지 않은 절충안으로 판단되었습니다. 예외보다 경고의 사용성이 훨씬 나쁘고, pytest와 같은 많은 테스트 프레임워크가 테스트 중 경고를 예외로 바꾸기 때문입니다.

__export__만 도입

이 제안의 원래 개정안에는 __export__ 를 독립 변수로 포함했으며 새로운 구문은 제공하지 않았습니다. 이것이 매력적이었던 이유는 하위 호환성이 있었기 때문입니다. 프로젝트는 간단히 __export__ = __all__를 작성할 수 있었으며, 사용자가 __export__를 지원하는 버전으로 업그레이드하면 이 PEP에서 설명한 문서화 및 강제 적용의 이점을 얻게 됩니다.

결국 이는 지나치게 보수적인 방안으로 판단되었습니다. __export____all__와 호환되기는 하지만, 목록에 항목을 추가하거나 제거하는 것을 잊는 것과 같은 많은 문제를 동일하게 공유하기 때문입니다.

Guido van Rossum의 말을 인용하면 quote 다음과 같습니다.

하지만 사용 편의성은 __all__의 사용 편의성과 비슷하며, 그것은 좋지 않습니다. 목록에 무언가를 추가하거나(또는 제거하는 것을!) 잊기 너무 쉽고, 내보내는 항목의 정의와 완전히 다른 파일 부분에서 내보내기 정보를 업데이트해야 하는 것이 산만합니다.

클래스 본문에 private 키워드 추가

이 제안에 대한 논의 중에 클래스에서 사용할 private 키워드를 추가하거나(export 를 클래스 본문에서 허용하자는 의견이 제시되었습니다. 예를 들면 다음과 같습니다.

class Something:
   private def hello(self):
      print("Hello, world!")

   export def goodbye(self):
      print("Goodbye, world!")

이는 이 PEP의 범위를 벗어나는 것으로 간주되며, 향후 제안에서 다시 다룰 수 있습니다.

내장 기능으로 publicprivate 데코레이터 추가

새로운 export 키워드를 추가하는 대신, Barry Warsaw의 atpublic 패키지를 기반으로 하는 privatepublic 데코레이터를 builtins 모듈에 추가하자는 의견이 제시되었습니다.

이 데코레이터를 사용하면 새로운 구문 없이도 이 PEP의 동일한 문서화 측면과 잠재적으로 동일한 강제 적용 측면을 제공할 수 있었을 것입니다. 예를 들면 다음과 같습니다.

@public
class MyPublicClass:
   ...

# Or
@private
class MyPrivateClass:
   ...

이는 export 구문 다음으로 저자가 선호하는 해결책이지만, 몇 가지 주의 사항이 있습니다. 특히 이름을 중복하지 않고 단순 변수를 내보낼 쉬운 방법이 없으며, 많은 사람이 이를 DRY 원칙 위반이라는 이유로 달가워하지 않습니다.

그럼에도 불구하고 이는 경쟁 제안인 PEP 844에 명시되어 있습니다.

모듈 이름을 기본적으로 공개로 설정

Python에서는 이름이 기본적으로 공개이므로 이 제안의 현재 동작이 직관에 반한다고 주장하는 사람이 많습니다. 이는 다른 언어와 비교할 때 export의 의미론이 성립하지 않음을 의미합니다( JavaScript가 이를 처리하는 방법 참조). 해결책으로 이름을 기본적으로 공개 상태로 유지하고, 어떤 이름이 비공개인지 설명하는 private 키워드(__private__ 목록과 함께)를 추가하자는 제안이 있었습니다. 예를 들면 다음과 같습니다.

def public_name():
   ...

private def private_name():
   ...

이는 이 제안의 의도된 목표(모듈에서 어떤 이름이 공개인지 더 명확하게 만드는 것)를 실제로 달성하기 훨씬 어렵게 만든다는 이유로 거부되었습니다.

이 제안의 목적은 비공개 이름이 공개 네임스페이스로 유출되는 것을 방지하는 것이지만, 제안된 __private__ 기능을 사용하면 개발자는 정의될 수 있는 모든 이름을 알고 고려해야 합니다. export에는 이러한 문제가 없습니다. 개발자는 공개되어야 할 몇 안 되는 이름만 고려하면 되며, 그 이후에는 비공개 여부를 다시 고려할 필요가 없습니다. __private____export__ 대신 사용한다면 모듈 작성자가 이름을 private로 표시하는 것을 자주 잊게 되고, 그 결과 사용자는 비공개 이름을 공개 이름이라고 생각하게 될 것으로 예상됩니다.

또한 가져오기, 도우미 함수 등으로 인해 대규모 모듈에는 공개 이름보다 비공개 이름이 훨씬 더 많은 경향이 있으므로 개발자에게 기계적으로도 더 어렵습니다.

마지막으로, export를 이해하기 어렵다고는 생각되지 않습니다. 앞서 언급했듯이 Python의 export는 JavaScript의 export 정의와 일치하지 않지만, 그 점을 제외하면 export가 무엇을 하며 왜 그렇게 하는지 이해하는 것은 Python 사용자에게 상당히 명확해 보입니다.

미해결 문제

패키지는 자체 비공개 멤버에 어떻게 접근해야 합니까?

패키지에 두 개의 모듈이 있다고 가정해 보십시오.

  1. library/utils.pylibrary 개발자만을 위한 유틸리티를 포함하도록 의도된 모듈입니다.
  2. library/main.pylibrary의 사용자가 사용할 수 있는 공개 API를 보유합니다.

utils.py의 이름은 library의 사용자가 접근하도록 의도된 모듈이 아니므로 내보내지지 않습니다. 그러나 main.py는 이러한 이름에 접근할 수 있어야 하며, 현재 제안에 따르면 utils.py에서 비공개 이름을 가져올 때 main.pyExportError를 받게 됩니다.

어떻게 해결해야 합니까? 이것이 정말 필요한 것입니까? 즉, utils.py가 해당 유틸리티를 내보낸 것으로 표시하고 사용자가 그 안의 어떤 것도 임포트하지 않도록 요청해야 합니까?

export에 최상위 표식이 필요합니까?

export의 사용은 모듈에 정의된 다른 모든 이름의 런타임 동작에 영향을 미치므로, 일부 경우 유지 관리를 더 어렵게 만들 수 있다는 주장이 제기되었습니다. 예를 들어 개발자가 모듈에서 이미 export를 사용하고 있는지 확신하지 못한다면, 나머지 코드에 영향을 주지 않고 공개 API를 export로 선언해도 안전한지 알기 위해 모듈에서 이를 검색해야 합니다.

해결책으로, 모듈 맨 위에 어떤 형태의 표식(예를 들어 __export__ = [] 선언이나 __future__ 임포트)이 있도록 export 구문을 요구하자는 제안이 나왔습니다. 위에서 설명한 문제가 실제로 실무에서 문제가 될지는 불분명하므로 아직 결정되지 않았습니다. 많은 라이브러리가 내부적으로 export/__export__ 사용 방식에 일관성을 보일 것으로 예상되므로, 개발자가 자신이 어떤 종류의 모듈에서 작업하는지 아는 것이 그리 어렵지는 않을 것입니다.

감사의 말

Hugo van Kemenade와 Savannah Ostrowski께서 이 PEP의 아이디어에 영감을 주신 데 감사드립니다.

또한 이 PEP의 설계는 Guido van Rossum, Paul Moore, Steve Dower, Barry Warsaw를 비롯한 많은 사람들의 논의와 아이디어에서 큰 영향을 받았습니다.

변경 이력

  • 15-Aug-2026
    • PEP를 철회했습니다.
  • 12-Aug-2026
    • export 문이 첨자 및 속성 할당과 함께 작동하는지 여부를 명확히 했습니다.
    • 파일 맨 위에서 export 구문이 필요한지에 관한 미해결 문제를 추가했습니다.
    • 실제 사례에 대한 예시를 더 추가했습니다.
    • “How To Teach This”에 __export__ 를 우회하는 방법에 관한 절을 추가했습니다.
  • 11-Aug-2026
    • __export__가 항상 list 객체가 되도록 요구했습니다.
    • 논의 스레드의 질문을 바탕으로 PEP의 일부 내용을 명확히 했습니다.
    • ExportWarning 을 제거하고 ExportError 내장 타입을 추가했으며, 내보내지 않은 이름에 접근할 때 예외를 발생시키는 방식으로 다시 전환했습니다.
  • 05-Aug-2026
    • export 문을 추가했습니다.
    • 내보내지 않은 속성에 접근할 때 RuntimeWarning 대신 ExportWarning 내장 타입이 발생하도록 추가했습니다.
  • 01-Aug-2026
    • 내보내지 않은 속성에 접근하면 이제 ImportError를 발생시키는 대신 RuntimeWarning을 발생시킵니다.
    • 동기 부여 절을 대폭 개편했습니다.