기본 콘텐츠로 건너뛰기

bytes.hex bytes_per_sep range

thumbnail

CPython: bytes.hex(bytes_per_sep) 허용 범위가 커졌다 — sys.maxsize도 이제 OK(gh-147944)

meta_description: Python 3.15에서 bytes.hex/bytearray.hex/memoryview.hex 및 binascii.b2a_hex의 bytes_per_sep 인자 허용 범위가 확대되어 sys.maxsize와 -sys.maxsize가 유효해졌다(gh-147944). 겉보기엔 테스트 한 줄 바뀐 수준이지만, 실제로는 Argument Clinic 변환(int→Py_ssize_t)과 정수 변환 경로가 정리되면서 “경계값에서의 Overflow/TypeError”가 더 일관되게 동작한다. 이 글은 무엇이 바뀌었는지, 실무에서 어디에 도움이 되는지(로그/헥스 덤프/프로토콜 디버깅), 그리고 안전한 사용 패턴을 정리한다. meta_keywords: python,bytes,bytearray,memoryview,hex,bytes_per_sep,binascii,b2a_hex,Py_ssize_t,sys.maxsize,OverflowError,Argument Clinic,debugging,hexdump,logging,protocol,binary,호환성,경계값,테스트 meta_robots: index,follow

바이너리 로그를 남기거나(패킷/토큰/압축 데이터), 디버깅용으로 bytes.hex()를 쓰다 보면 이런 걸 해본 적이 있을 거다.

  • “너무 길어서 보기 힘드니, 구분자(sep)를 넣어서 그룹으로 끊자”

예:

payload.hex(':', 2)   # b9:01ef 처럼 2바이트마다 구분
payload.hex(' ', -2)  # 반대 방향(왼쪽부터)으로 2바이트마다

이때 bytes_per_sep에 “엄청 큰 값”을 넣으면, 사실상 구분자를 넣지 않는 효과가 난다.

  • bytes_per_sep가 길이보다 크면 → 구분자 없음

그런데 CPython 내부 구현/클리닉 파서가 int 기준이던 시절에는, 플랫폼에 따라 “큰 값”이 경계에서 애매하게 취급될 여지가 있었다.

이번 PR(gh-147944)은 이 허용 범위를 명확히 넓힌다.

  • 이제 sys.maxsize-sys.maxsize가 유효한 값으로 인정된다
  • 대상 API:
    • bytes.hex
    • bytearray.hex
    • memoryview.hex
    • binascii.b2a_hex / binascii.hexlify


1) 뭐가 바뀌었나: bytes_per_sep가 int가 아니라 Py_ssize_t로 들어온다

PR diff를 보면 핵심은 이거다.

  • bytes_per_sep 파라미터 타입이 intPy_ssize_t로 바뀐다
  • Argument Clinic이 PyLong_AsInt() 대신 “인덱스 정수”로 받아서 PyLong_AsSsize_t()로 변환한다

그 결과로 문서/테스트도 이렇게 바뀐다.

  • 기존 경계값 테스트: 2**31-1 같은 32비트 int 범위 중심
  • 변경: sys.maxsize 중심(플랫폼 의존 경계를 파이썬 레벨로 끌어올림)

NEWS에도 명시된다:

  • sys.maxsize-sys.maxsize가 이제 유효

2) 왜 이게 실무에서 도움이 되나: ‘경계값’이 안정되면 디버깅 코드가 덜 깨진다

이 패치는 성능 패치가 아니다.

하지만 실무에서는 이런 류의 안정성/일관성 패치가 은근히 체감된다.

(A) 플랫폼/빌드에 따른 애매함을 줄인다

현실의 파이썬은

  • 64-bit 리눅스
  • 64-bit 윈도우
  • 때로는 특수 임베딩 환경

처럼 다양한 환경에서 돌아간다.

int 기반 API는 이런 곳에서 “큰 값”의 취급이 경계에서 흔들릴 수 있다.

이번 변경은 bytes_per_sep를 아예 “사이즈(길이) 단위”로 받도록 맞추면서, sys.maxsize 같은 파이썬 레벨의 기준으로 통일한다.

(B) hexdump/logging 유틸에서 ‘off’ 스위치를 더 안전하게 만들 수 있다

실전에서는 이런 옵션이 자주 있다.

  • group_bytes: 그룹 끊기 단위
  • sep: 구분자

그리고 “grouping을 끄고 싶다”는 요구도 많다.

나쁜 방법은 None 같은 특별값을 끼워 넣는 건데, API가 그걸 안 받으면 결국 조건문이 늘어난다.

이제는 아예 “극단적으로 큰 bytes_per_sep”를 하나의 스위치로 쓸 수 있다.

import sys

def hex_dump(buf: bytes, group: int | None = 2) -> str:
    if group is None:
        return buf.hex(':', sys.maxsize)  # 사실상 sep 없음
    return buf.hex(':', group)

(물론 sep=None을 쓰면 더 직접적으로 구분자 없이 갈 수도 있다. 하지만 유틸 설계에 따라선 “항상 sep는 있고 group만 바꾼다”가 편할 때가 있다.)


3) bytes.hex / binascii.hexlify에서 bytes_per_sep를 쓸 때 헷갈리는 포인트

포인트 1) 양수/음수의 의미

  • bytes_per_sep > 0: 오른쪽부터 그룹을 센다
  • bytes_per_sep < 0: 왼쪽부터 그룹을 센다

이건 길이가 고정되지 않은 바이너리(가변 길이 필드)에서 꽤 유용하다.

  • 오른쪽 정렬(예: 체크섬/하위 바이트 의미가 큰 경우)
  • 왼쪽 정렬(예: 헤더가 앞에 몰려있는 경우)

포인트 2) sep는 “한 글자/한 바이트” 제약이 있다

이건 그대로다.

  • sep=':' 또는 sep=b':' 같은 한 글자

포인트 3) 너무 큰 값은 “그룹 없음”이 된다

이게 이번 패치의 실사용 포인트다.

  • bytes_per_sep가 버퍼 길이보다 크면 구분자가 삽입되지 않는다

4) 실전 예시 3개: 로그/프로토콜/테스트에서 bytes_per_sep를 이렇게 쓴다

예시 A) 로그를 ‘눈으로 읽게’ 만들기(2바이트 그룹)

16진수 덤프는 그냥 찍으면 너무 길다.

payload = b"\xb9\x01\xef\x00\x10\x20\x30\x40"
print(payload.hex(':', 2))
# b901:ef00:1020:3040

대부분의 프로토콜이 2바이트/4바이트 단위로 의미가 생기기 때문에, 2바이트 그룹이 생각보다 자주 먹힌다.

예시 B) 헤더(앞부분)만 보기 좋게(왼쪽 기준)

헤더가 앞에 몰려 있고 뒤는 페이로드라서 의미가 약할 때는, 왼쪽 기준 그룹이 더 편하다.

print(payload.hex(' ', -2))
# b901 ef00 1020 3040

예시 C) 테스트에서 “grouping을 끄는 스위치”로 쓰기

테스트에서 출력 포맷을 비교할 때, 옵션 조합이 많아지면 코드가 지저분해진다.

3.15+ 환경이라면 이렇게 ‘off’를 값으로 표현할 수 있다.

import sys

def hexfmt(buf: bytes, sep: str = ':', group: int | None = 2) -> str:
    if group is None:
        # 사실상 grouping off
        return buf.hex(sep, sys.maxsize)
    return buf.hex(sep, group)

5) 호환성 전략: 라이브러리는 보수적으로, 내부 서비스는 적극적으로

이 PR은 3.15 whatsnew에 들어간 변경이다.

  • 즉, 3.14 이하에서는 sys.maxsize가 반드시 허용된다고 장담하기 어렵다(플랫폼/구현에 따라)

그래서 추천 전략은 단순하다.

  • 오픈소스 라이브러리/외부 배포 코드: bytes_per_sep에 굳이 sys.maxsize를 넣지 말고, 조건문으로 분기
  • 내부 서비스/환경이 3.15+로 고정되면: sys.maxsize를 “off 스위치”로 써도 된다

6) 결론: ‘보기 좋은 헥스’는 디버깅 시간을 줄인다

바이너리 문제는 대개 이런 식으로 시작한다.

  • “뭔가 한 바이트가 다르다”
  • “구분자를 어떻게 끊어 보냐에 따라 갑자기 보인다”

그래서 hex 출력의 옵션이 조금만 더 유연해져도, 디버깅 시간이 줄어든다.

이번 gh-147944는 그런 종류의 작은 개선이다.

  • 경계값이 명확해지고
  • 플랫폼 의존이 줄고
  • API 사용이 조금 더 일관돼진다

결국 이런 변화가 쌓여서 표준 라이브러리가 “운영 친화적”이 되는 거라고 본다.

마지막으로, 이런 류의 변경은 단순히 sys.maxsize를 허용하는 걸 넘어서서, 실패 모드도 더 예측 가능하게 만든다.

  • sys.maxsize는 OK
  • sys.maxsize + 1 같은 값은 상황에 따라 OverflowError/예외로 떨어질 수 있고
  • 말도 안 되는 큰 값(예: 2**1000)은 확실히 예외가 난다

즉, 경계가 “대충”이 아니라 “명시적으로” 정리된다.

운영/디버깅 코드에서 이런 차이가 꽤 크다. 어설프게 동작하다가 모서리에서 깨지는 것보다, 빨리 예외로 떨어져서 원인을 드러내는 편이 낫다.

한 줄 요약:

  • “hex 출력 포맷은 취향이 아니라 디버깅 비용이다.”

Keywords

python,bytes,bytearray,memoryview,hex,bytes_per_sep,binascii,b2a_hex,Py_ssize_t,sys.maxsize,OverflowError,Argument Clinic,debugging,hexdump,logging,protocol,binary,compatibility


이미지 크레딧/라이선스

  • Mobile developer at work (Unsplash).jpg — Parker Byrd / CC0
    • https://commons.wikimedia.org/wiki/File:Mobile_developer_at_work_(Unsplash).jpg
    • http://creativecommons.org/publicdomain/zero/1.0/deed.en

댓글

이 블로그의 인기 게시물

Django에서 트랜잭션 관리하기

Django에서 트랜잭션 관리하기 안녕하세요! 오늘은 Django에서 데이터베이스 트랜잭션을 효과적으로 관리하는 방법에 대해 알아보겠습니다. 1. 트랜잭션의 중요성 트랜잭션은 데이터베이스의 일관성과 무결성을 보장하는 중요한 개념입니다. Django에서는 여러 가지 방법으로 트랜잭션을 관리할 수 있습니다. 1.1 기본 개념 원자성(Atomicity) : 트랜잭션은 모두 실행되거나 모두 실행되지 않아야 합니다. 일관성(Consistency) : 트랜잭션 전후로 데이터베이스의 일관성이 유지되어야 합니다. 격리성(Isolation) : 동시에 실행되는 트랜잭션들이 서로 영향을 주지 않아야 합니다. 지속성(Durability) : 완료된 트랜잭션의 결과는 영구적으로 저장되어야 합니다. 2. Django의 트랜잭션 관리 2.1 기본 설정 # settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'mydatabase', 'USER': 'myuser', 'PASSWORD': 'mypassword', 'HOST': 'localhost', 'PORT': '5432', 'ATOMIC_REQUESTS': True, # 모든 뷰를 트랜잭션으로 래핑 } } 2.2 데코레이터 사용 from django.db import transaction @transaction.atomic def create_order(user, items): order = Order.objects.create(user=...

Python에서 asyncio 완전 정복 (await, async, gather 등)

어휴, 요즘 파이썬으로 비동기 프로그래밍 하는 재미에 푹 빠졌어요! 특히 asyncio 는 정말 마법 같더라고요. 처음엔 좀 낯설었는데, 익숙해지니까 속도 향상이 눈에 띄게 느껴져서 완전 반해버렸습니다. 이 글에선 제가 asyncio 를 배우면서 깨달은 점들을 풀어놓을게요. 혹시 비동기 프로그래밍이 뭔지 잘 모르시겠다면, 간단히 말해 여러 작업을 동시에 처리해서 프로그램 속도를 엄청나게 높이는 기술이라고 생각하시면 돼요. 마치 여러 요리사가 동시에 음식을 만들어서 손님에게 빨리 제공하는 것과 비슷하죠! 일단 async 와 await 라는 녀석들이 핵심인데요, async 는 함수 앞에 붙여서 "얘는 비동기 함수야!"라고 선언하는 거예요. 그리고 await 는 다른 비동기 함수가 끝날 때까지 기다리라고 지시하는 역할을 하죠. 예를 들어, 네트워크에서 데이터를 가져오는 함수가 있다면, await 를 사용해서 데이터가 다 가져올 때까지 기다렸다가 다음 작업을 진행할 수 있어요. 그 동안 다른 작업을 처리할 수 있으니, 마치 멀티태스킹을 하는 것처럼 느껴져요. 신기하지 않나요? 그리고 asyncio.gather 는 여러 비동기 함수를 동시에 실행하고 결과를 모아주는 아주 유용한 친구입니다. 제가 웹사이트 여러 개에서 데이터를 동시에 가져와야 할 때 정말 요긴하게 썼어요. 하나씩 순서대로 가져오는 것보다 훨씬 빠르더라고요! 마치 여러 개의 탭을 동시에 열어놓고 작업하는 것과 같다고 생각하시면 될 것 같아요. 실제로 제가 썼던 코드를 보여드릴게요. 세 개의 웹사이트에서 데이터를 가져오는 예제인데요. (아래 코드 삽입) 이 코드를 보시면, fetch_data 함수가 각 웹사이트에서 데이터를 가져오는 역할을 하고, asyncio.gather 가 이 함수들을 동시에 실행하도록 도와주는 것을 볼 수 있을 거예요. asyncio.sleep(2) 는 네트워크 지연을 시뮬레이션하기 위해 넣...

AWS S3 + CloudFront로 정적 파일 서빙 완전 가이드

AWS S3 + CloudFront로 정적 파일 서빙 완전 가이드 안녕하세요! 오늘은 AWS S3와 CloudFront를 사용하여 정적 파일을 효율적으로 서빙하는 방법에 대해 알아보겠습니다. 왜 S3와 CloudFront를 사용할까요? 높은 가용성 : AWS의 글로벌 인프라를 활용 빠른 전송 속도 : CloudFront의 CDN 기능으로 전 세계 사용자에게 빠른 전송 비용 효율성 : 사용한 만큼만 지불 보안 : AWS의 보안 기능 활용 확장성 : 트래픽 증가에 자동 대응 1. S3 버킷 설정 1.1 버킷 생성 및 설정 import boto3 def create_s3_bucket(): s3 = boto3.client('s3') # 버킷 생성 bucket_name = 'your-static-files-bucket' s3.create_bucket( Bucket=bucket_name, CreateBucketConfiguration={ 'LocationConstraint': 'ap-northeast-2' } ) # 버킷 정책 설정 bucket_policy = { "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObje...