PEP 686: Python 3.15에서 UTF-8 모드 기본 활성화
(peps.python.org)- Python 3.15는 UTF-8 모드를 기본으로 켜 파일, 표준 입출력, 파이프의 기본 인코딩을 UTF-8로 맞춤
- UTF-8은 소스 파일, JSON·TOML·YAML, 주요 편집기, 웹 데이터, Node.js·Go·Rust·Java 등에서 사실상 표준 인코딩으로 쓰이고 있어 상호운용성이 커짐
- 기존 기본 인코딩은 플랫폼에 따라 달라 Unix 개발자가
encoding="utf-8"을 생략하면 Windows 등에서 불일치 버그가 생길 수 있음 - 필요하면
PYTHONUTF8=0또는-X utf8=0으로 끌 수 있으며, 호환성 점검에는EncodingWarning,encoding="utf-8",encoding="locale",locale.getencoding()이 쓰임 - 기본 인코딩에 의존하는 프로그램은 주로 Windows에서
UnicodeError, mojibake, 조용한 데이터 손상을 겪을 수 있어 사전 점검이 필요함
Python 3.15에서 바뀌는 기본 인코딩
- PEP 686은 PEP 540의 UTF-8 모드를 기본값으로 활성화하는 변경임
- 기본 인코딩이 필요한 파일, stdio, 파이프에서 Python은 UTF-8을 일관되게 사용함
- Python 3.15부터 기본 활성화되며, 사용자는 다음 방식으로 비활성화할 수 있음
PYTHONUTF8=0-X utf8=0
UTF-8을 기본값으로 삼는 이유
- UTF-8은 여러 환경에서 표준 텍스트 인코딩처럼 자리 잡음
- Python 소스 파일의 기본 인코딩은 UTF-8임
- JSON, TOML, YAML은 UTF-8을 사용함
- Visual Studio Code와 Windows Notepad를 포함한 대부분의 텍스트 편집기가 UTF-8을 기본으로 사용함
- 인터넷의 대부분 웹사이트와 텍스트 데이터가 UTF-8을 사용함
- Node.js, Go, Rust, Java를 포함한 여러 인기 프로그래밍 언어가 UTF-8을 기본으로 사용함
- Python의 기본 인코딩이 UTF-8로 바뀌면 다른 도구·언어·데이터 형식과의 상호운용성이 좋아짐
- 많은 Unix 환경 Python 개발자는 기본 인코딩이 플랫폼 의존적이라는 점을 잊고, JSON·TOML·Markdown·Python 소스 파일처럼 UTF-8 텍스트를 읽을 때
encoding="utf-8"을 생략함 - 플랫폼별 기본 인코딩 차이는 이런 코드가 다른 환경에서 깨지는 버그 원인이 됨
locale API와 encoding="locale" 수정
- UTF-8 모드는
locale.getpreferredencoding(False)에 영향을 주므로, UTF-8 모드와 무관하게 locale 인코딩을 얻는 API가 필요함 locale.getencoding()은 이 목적을 위해 추가됐고, locale 인코딩을 반환하되 UTF-8 모드는 무시함- 이 API는 Python 3.11에 추가됨
warn_default_encoding옵션이 지정되면locale.getpreferredencoding()은open()처럼 PEP 597의EncodingWarning을 발생시킴- PEP 597은
TextIOWrapper에encoding="locale"옵션을 추가해 locale 인코딩을 명시적으로 지정할 수 있게 했음 - 기존에는 UTF-8 모드에서
encoding="locale"을 지정해도TextIOWrapper가"UTF-8"을 사용했음- 이는 PEP 597의 동기와 맞지 않았음
- Python의 기본 텍스트 인코딩이 바뀔 때 UTF-8 모드가 기본값이 되는 상황을 예상하지 못했기 때문임
- 이 불일치는 Python 3.11에서 수정됐고, UTF-8 모드에서도
encoding="locale"을 넘기면 locale 인코딩을 사용함
하위 호환성과 마이그레이션 절차
- 대부분의 Unix 시스템은 UTF-8 locale을 사용하고, Python은 locale이 C 또는 POSIX일 때 UTF-8 모드를 활성화하므로 변경 영향은 주로 Windows 사용자에게 집중됨
- 기본 인코딩에 의존하는 Python 프로그램은 다음 문제를 겪을 수 있음
UnicodeError- mojibake
- 조용한 데이터 손상
- 하위 호환성 문제를 고치는 권장 절차는 다음과 같음
- UTF-8 모드를 비활성화함
- PEP 597의
EncodingWarning으로 UTF-8 모드가 영향을 주는 위치를 찾음encoding옵션이 생략된 경우encoding="utf-8"또는encoding="locale"사용을 검토함locale.getpreferredencoding()을 사용한 경우"utf-8"또는locale.getencoding()사용을 검토함
- UTF-8 모드에서 애플리케이션을 테스트함
Ruby·Java 선행 사례와 거절된 대안
- Ruby는 Ruby 3.0, 2020년에 Windows의 기본
external_encoding을 UTF-8로 변경함 - Java는 JDK 18, 2022년에 기본 텍스트 인코딩을 UTF-8로 변경함
- Ruby와 Java 모두 하위 호환성을 위한 옵션은 제공하지만, Python의
EncodingWarning같은 기본 인코딩 사용 경고는 제공하지 않음 - 기본 인코딩 사용 자체를 폐기하는 방안은 거절됨
- ASCII 텍스트만 읽고 쓰는 용도로 기본 인코딩을 쓰는 경우가 많음
- Unix에서만 실행되는 비크로스플랫폼 애플리케이션에는 그런 경고가 유용하지 않음
- 모든 곳에
encoding을 강제하면 사용자 부담이 크고, 많은DeprecationWarning은 사용자가 경고를 무시하게 만들 수 있음 - PEP 387은 하위 호환성을 깨는 변경에 경고 추가를 요구하지만, 반드시
DeprecationWarning을 요구하지는 않음
subprocess모듈의 파이프 기본 인코딩으로PYTHONIOENCODING을 쓰는 방안도 거절됨- 이 방식은 UTF-8 모드에서도
subprocess.Popen(text=True)에 레거시 인코딩을 쓸 수 있게 함 - 하지만 “기본 인코딩”을 복잡하게 만들고, 이 방식 자체도 하위 호환성을 깨는 변경임
- 사용자는
text=True를encoding="utf-8"또는encoding="locale"로 바꿀 때까지 UTF-8 모드를 끌 수 있음
- 이 방식은 UTF-8 모드에서도
사용자 교육 관점
- 신규 사용자는 처음 1년 동안 텍스트 인코딩을 배울 필요가 줄어듦
- 비 UTF-8 텍스트 파일을 다뤄야 할 때 인코딩을 배우면 됨
- 기존 사용자는 하위 호환성 절차에 따라 영향을 받는 지점을 점검해야 함
댓글과 토론
Hacker News 의견들
-
기본 텍스트 파일 인코딩이 플랫폼에 따라 달라지는 것은 늘 짜증났는데, 이번 변경은 반가움
파일시스템 인코딩까지 건드리려 하지 않은 점도 좋음. 그건 별개의 문제이고, 그것대로 골치 아픔- Windows의 시스템 기본 코드 페이지는 플랫폼뿐 아니라 시스템 로캘에도 의존함
Windows가TextOutA같은 ANSI 함수들이 UTF-8 코드 페이지를 쓰도록 간단히 선택할 방법을 오랫동안 제공하지 않은 건 큰 실수였음. manifest 파일로 가능해진 건 Windows 10 개발 중반쯤의 일인데, 이런 기능은 NT4나 Windows 98 시절에 들어갔어야 함 - 역사적으로는 말이 됐음. 대부분의 소프트웨어가 로컬 전용이었고, 텍스트 파일도 로컬 인코딩일 것으로 기대됐기 때문임
플랫폼뿐 아니라 사용자의 선호 로캘에 의존했고, C 표준 라이브러리도 같은 방식으로 동작함. 예를 들어 Unix/Linux에서는 서유럽 언어에서iso-8859-1이 흔했고, 유로 도입 후에는€기호가 있는iso-8859-15로 바꾸는 일이 많아졌음. UTF-8이 문제없이 동작하기 시작한 건 2000년대 후반쯤이고, Debian은 Etch 릴리스에서 기본값을 UTF-8로 바꿈 - 며칠 전에는 줄바꿈을 암묵적으로 바꾸는 것에 당했음
회사 노트북에서 로컬 테스트는 전부 잘 됐는데 Linux 호스트에 배포하니 하위 애플리케이션이 CRLF를 요구해서 소비하지 못했음. 가끔 기억해야 하는 사소하고 멍청한 문제 중 하나임. 다만 새로 작성된 소프트웨어가 왜 특정 줄 종결자를 요구하는지도 타당한 질문임 - Windows에서 누군가 코드를 쓰기 시작하면 이 문제에 여러 번 당했음
- Windows의 시스템 기본 코드 페이지는 플랫폼뿐 아니라 시스템 로캘에도 의존함
-
불안정한 시스템 기본값에 기대지 않는 건 좋은 일임
이런 값들은 어느 순간 내가 가정한 것과 다르게 돌아오는 경향이 있음. 몇 년 전 Ubuntu와init.d스크립트를 다룰 때, Java를 띄우는 스크립트가 root로 실행되고 있었고 Docker 이전이라 더 그랬는데, 일반 사용자에게는 정상적인 UTF-8 기본값을 설정하지 않는 셸에서 실행됐음. 그 결과 OS 기본값을 쓰는 Java의 나쁜 API 사용이 드러났음
요즘은 대부분 인코딩을 명시할 수 있는 변형 API가 있고, 정적 코드 검사기도 잘못된 것을 쓰면 경고함. 하지만 한 군데만 빠져도 콘텐츠가 망가지기 시작함. 이제 UTF-8이 아닌 인코딩 사용은 대부분 의도치 않은 경우일 가능성이 매우 높고, 의도한 것이라면 OS의 이상한 간접 설정에 기대지 말고 명시해야 함. 그래서 좋은 변경이고, 이로 깨지는 코드는 간단한 수정이 붙는 편이 낫다- PowerShell에서 별칭으로 만든
touch함수가 생성한.gitignore를 쓰고 있었는데, 아무리 해도 Git이 이를 존중하지 않았음
확인해 보니 생성된 텍스트 파일이 UTF-16이라 사실상 무시되고 있었음. 교훈을 얻고 시스템 기본값을 UTF-8로 바꿨지만, 지금은 그냥 텍스트 편집기에 의존함 - 전역 로캘은 인코딩만이 아니라 전반적으로 실수였음
printf("%f", 4.2)가 환경에 따라 마법처럼 다른 문자열을 출력하면 해결보다 문제가 더 많아짐. 로캘 의존 동작을 원할 때는 함수에 로컬 정보나 관련 부분을 명시적으로 넘겨야 함
- PowerShell에서 별칭으로 만든
-
지난 수십 년 동안 점점 더 맞아 들어간 휴리스틱이 있음: 어딘가에
charset설정이 있는데 UTF-8이 아니면 틀린 것임
Python 2는 문자 집합에 무관해서 늘 동작했지만, Python 3의 개선은 단순한 개선만은 아니었음. Python 3 스크립트와 Python 2 스크립트를 구분하는 법은 이렇다: 문자열utf-8이 들어 있으면 Python 3이고,C.UTF-8로캘에서만 동작하면 Python 3임. 이번 변경은 Python 3를 “수리”하는 것처럼 이해돼서 환영함 -
Python 3부터 기본값인 줄 알았음
- 아마 Python 3에서
u""접두사가 불필요해진 문자열을 떠올린 듯함
방금 Python 2.7에서"éķů"를 입력해 보니 해당 문자의 UTF-8 바이트를 출력해서u접두사가 정확히 뭘 했는지는 잘 모르겠지만, Python 2에서 3으로 넘어가며 큰 변화 중 하나는 문자열이 인코딩을 갖고, 바이트 문자열은 인코딩 없는 바이트 시퀀스가 된 것임. 이번 변경은 주로 Windows처럼 기본 인코딩이 UTF-8이 아닌 환경에서open('filename', mode='r')을 쓸 때open('filename', mode='r', encoding='UTF-8')을 명시해야 하던 문제에 관한 것으로 보임 - Python 3에서 Python 소스 코드는 기본적으로 UTF-8임. 하지만 파일에 저장할 때 쓰는 문자 인코딩에 대해서는 아무 말도 해주지 않고, 기본값은 로캘 의존임
Path("filenames use their own encoding").write_text("file content encoding uses yet another encoding")처럼 문자열 리터럴, 파일명, 파일 내용 인코딩은 각각 다름. 대응되는 인코딩은tokenize.open의 UTF-8,os.fsencode의sys.getfilesystemencoding(),open의locale.getpreferredencoding()임
- 아마 Python 3에서
-
“Node.js, Go, Rust, Java를 포함한 다른 인기 프로그래밍 언어도 UTF-8을 기본으로 쓴다”라니, Java가 UTF-16에서 UTF-8로 옮긴 것을 놓쳤음
- Java에서 바이트를 문자열로 변환할 때의 기본 인코딩은 원래 플랫폼 의존이었고, 지금은 UTF-8임
String클래스 내부에서는 UTF-16과 latin-1 인코딩이 여전히 쓰이고, JVM은 예전처럼 변형 UTF-8 인코딩을 사용함.String클래스는 원래 UTF-16만 썼지만 Java 9부터 가능할 때는 문자당 1바이트 latin-1 인코딩도 사용함 - 내부 문자열 표현과 읽기/쓰기 인코딩을 섞어 말하는 듯함
Java는 읽기/쓰기 인코딩의 기본값으로 UTF-16을 쓴 적이 없음 - Java 18에서 2년 전에 바뀐 것으로 보임
- Java에서 바이트를 문자열로 변환할 때의 기본 인코딩은 원래 플랫폼 의존이었고, 지금은 UTF-8임
-
CPython의 내부 인코딩은 이제 UTF-8인가?
Python 문자열은 첨자로 인덱싱할 수 있지만 임의 접근은 충분히 드물어서 필요할 때 지연 인덱싱해도 괜찮을 듯함. 한 칸 앞으로 가거나 뒤로 가기만 하면 인덱스가 필요 없으니, 내부 표현을 UTF-8로 하는 것도 충분히 가능함str을 표현하는 것은PyUnicode객체임
UTF-8 바이트가 요청되면bytes객체가 필요 시 생성되고PyUnicode의 일부로 캐시되며,PyUnicode가 해제될 때 함께 해제됨. 별도로 문자열을 구성하는 코드 포인트들은 임의 접근이 가능하도록 단순 배열에 저장됨. 각 코드 포인트 크기는 1, 2, 4바이트가 될 수 있고,PyUnicode를 만들 때 최대 코드 포인트 값을 지정하면 127, 255, 65535, 1,114,111 중 하나로 올림되어 1/2/4바이트 사용 여부가 결정됨
최대 코드 포인트 값이 127이면 그 배열 표현을 UTF-8로 직접 쓸 수 있음. 그래서 질문에 대한 답은, 모든 코드 포인트가 127 이하인 많은 문자열은 UTF-8로 저장된다는 것임. 다만 문자열을 순회할 때 코드 포인트 단위로 하면 안 됨. 사용자가 인식하는 문자, 즉 그래핌 클러스터는 하나 이상의 코드 포인트로 이루어짐. 예를 들어 악센트가 붙은 e는e코드 포인트 뒤에 결합 악센트 코드 포인트가 올 수 있고, 불사조 이모지는 새 이모지, 폭 없는 결합자, 불 이모지로 구성됨. 수억 명이 쓰는 일부 문자 체계도 자음에 모음을 나타내는 결합 기호가 붙는 방식과 비슷함. 이- -는 코드 포인트 5개이고, 여러 언어가 그 “길이”를 어떻게 보고하는지 다룬 좋은 글이 있음: https://hsivonen.fi/string-length/. 이 부분을 다루는 Unicode TR29를 Python C 확장으로 막 구현해 본 경험에서 나온 내용임
-
왜
utf-8-sig가 아닌지 궁금함. 선택적 BOM을 처리해 주는데, 지난주에도 그것 때문에 스크립트를 고쳐야 했음- 이제는 어떤 것도 UTF-8에 BOM을 넣어서는 안 됨
권장되지도 않고, 요즘은 BOM에서 실패하는 동작도 합리적이라고 봄 - Python이 모든 입출력 앞에 보이지 않는 BOM을 조용히 붙이도록 바꾸는 건 좋은 생각이 아님
- 이제는 어떤 것도 UTF-8에 BOM을 넣어서는 안 됨
-
UTF-8 얘기라면, Linux 프레임버퍼는 오래전에 제대로 된 UTF-8 지원을 갖췄어야 함
256/512 글리프짜리 말고 진짜 지원 말임. GNU Hurd조차 2007년쯤부터 UTF-8을 지원하는 더 나은 터미널 콘솔이 있었는데, 지금은 2024년임 -
좋음. 이제 남은 건 JS가 UTF-8로 바꾸는 것뿐임
물론 JS는 개선될 수 없음. 다른 어떤 프로그래밍 언어와 달리 1995년에 작성된 코드와 호환돼야 하니까- 이건 Python에 파일을 “텍스트로” 열어 달라고 했을 때 기본으로 어떤 인코딩을 쓸지에 관한 얘기임
문자열의 내부 표현은 별개의 문제이고, JavaScript처럼 Python도 내부에서 “그냥 UTF-8”을 쓰지는 않음
- 이건 Python에 파일을 “텍스트로” 열어 달라고 했을 때 기본으로 어떤 인코딩을 쓸지에 관한 얘기임
-
“Unix를 쓰는 많은 Python 개발자들이 기본 인코딩이 플랫폼 의존이라는 사실을 잊고, UTF-8로 인코딩된 텍스트 파일을 읽을 때
encoding="utf-8"을 생략한다”에서, 이건 잊었다기보다 충분히 알려지지 않은 것일 수도 있음
솔직히 Python은 명시적으로 다르게 요청하지 않는 한 모든 곳에서 UTF-8만 쓴다고 생각했음- 실제로는 경우에 따라 다름
bytes.decode와str.encode는 적어도 Python 3부터 UTF-8을 기본값으로 써 왔음. 반면 파일 이름을 디코딩할 때의 기본 인코딩은sys.getfilesystemencoding()을 쓰며, Windows와 macOS에서는 이것도 UTF-8이지만 Linux에서는 로캘, 구체적으로CODESET에 따라 달라짐. 마지막으로open은locale.getencoding()을 직접 사용함
- 실제로는 경우에 따라 다름