Python Python

Care este diferența dintre `str` și `bytes` în Python și când convertești între ele

Dima Авг 30, 2026

Pe scurt

str este o secvență de puncte de cod Unicode — text, fără nicio codificare atașată. bytes este o secvență de întregi între 0 și 255 — date brute, fără niciun înțeles textual. Trecerea de la text la octeți se face cu encode(codificare), invers cu decode(codificare), iar codificarea trebuie știută: nu este stocată nicăieri în valoarea bytes. Cele două tipuri nu se amestecă niciodată implicit, iar regula practică este să decodifici imediat ce datele intră în program și să codifici doar la ieșire — tot ce este în mijloc lucrează cu str.


Cum funcționează

Un str stochează puncte de cod, nu caractere tipărite și nu octeți. CPython alege intern reprezentarea cea mai compactă care acoperă cel mai mare punct de cod din șir — un octet, doi sau patru per poziție — dar aceasta este o optimizare invizibilă. len numără puncte de cod, s[i] returnează un str de lungime 1, iar comparațiile și dispersia operează pe puncte de cod.

Un bytes stochează octeți. len numără octeți, iar b[i] returnează un int, nu un bytes de lungime 1 — asimetria față de str este deliberată și surprinde constant. Feliile, în schimb, returnează bytes, deci b[0:1] și b[0] au tipuri diferite.

encode aplică un algoritm de codificare punctelor de cod și produce octeți; decode face drumul invers. Rezultatul lui encode nu poartă nicio informație despre codificarea folosită. Un fișier, un socket sau un câmp de bază de date care conține octeți este ambiguu până când cineva declară cum trebuie citit — de aceea codificarea aparține protocolului sau convenției, nu datelor.

Nu orice text intră în orice codificare, și nu orice secvență de octeți este validă în orice codificare. latin-1 acoperă doar primele 256 de puncte de cod, deci ș ridică UnicodeEncodeError. Invers, octeții UTF-8 ai lui ș nu formează o secvență ASCII validă, deci decode("ascii") ridică UnicodeDecodeError. Periculoasă este combinația care nu eșuează: latin-1 decodifică orice secvență de octeți, pentru că mapează fiecare octet la punctul de cod cu același număr — produce text greșit fără nicio eroare.

Parametrul errors controlează ce se întâmplă la eșec, iar alegerea lui determină dacă informația se pierde. replace și ignore distrug octeții problematici definitiv; surrogateescape îi ascunde în puncte de cod din zona substituenților, de unde pot fi recuperați exact la recodificare. Invariantul de reținut: str este text, bytes este transport; conversia este singurul loc unde codificarea contează, deci ține-o la marginile programului.


Cum arată la rulare

Creează un fișier:

touch depot_payload.py

Rulează-l cu:

python3 depot_payload.py

Două tipuri, două unități de măsură

Cel mai mic caz: același text în ambele reprezentări, măsurat și indexat.

label = "Iași depot"
raw = label.encode("utf-8")

print("str type :", type(label).__name__, "len:", len(label))
print("bytes type:", type(raw).__name__, "len:", len(raw))
print("raw       :", raw)
print("raw[0]    :", raw[0], type(raw[0]).__name__)
print("label[0]  :", label[0], type(label[0]).__name__)
print("round trip:", raw.decode("utf-8") == label)

try:
    label + raw
except TypeError as exc:
    print("TypeError:", exc)


str type : str len: 10
bytes type: bytes len: 11
raw       : b'Ia\xc8\x99i depot'
raw[0]    : 73 int
label[0]  : I str
round trip: True
TypeError: can only concatenate str (not "bytes") to str


  • Cele două lungimi diferă cu unu, exact octetul suplimentar al lui ș în UTF-8. Textul are 10 puncte de cod, transportul are 11 octeți.
  • raw[0] este 73, un int — indexarea unui bytes dă numărul octetului. label[0] este 'I', un str de lungime 1.
  • TypeError-ul final este garanția centrală a modelului: nu există conversie implicită între text și octeți, deci un bug de codificare apare ca eroare de tip, nu ca text corupt.

Reprezentarea b'Ia\xc8\x99i depot' arată octeții ASCII ca litere și pe ceilalți ca escape-uri. Este o comoditate de afișare, nu o proprietate a valorii — toți sunt octeți.


Codificarea trebuie să se potrivească textului

Următorul nivel adaugă mai multe codificări și ambele feluri de eșec. Înlocuiește fișierul cu:

label = "Iași depot"

for encoding in ("utf-8", "utf-16-le", "latin-1"):
    try:
        encoded = label.encode(encoding)
        print(f"{encoding:10} -> {len(encoded):2} bytes  {encoded[:8]!r}...")
    except UnicodeEncodeError as exc:
        print(f"{encoding:10} -> UnicodeEncodeError: {exc.reason} at position {exc.start}")

utf8 = label.encode("utf-8")
try:
    utf8.decode("ascii")
except UnicodeDecodeError as exc:
    print("decoding utf-8 as ascii:", exc.reason, "at position", exc.start)

print("decoded as latin-1 silently:", repr(utf8.decode("latin-1")))


utf-8      -> 11 bytes  b'Ia\xc8\x99i de'...
utf-16-le  -> 20 bytes  b'I\x00a\x00\x19\x02i\x00'...
latin-1    -> UnicodeEncodeError: ordinal not in range(256) at position 2
decoding utf-8 as ascii: ordinal not in range(128) at position 2
decoded as latin-1 silently: 'IaÈ\x99i depot'


  • utf-16-le produce aproape dublu, pentru că folosește cel puțin doi octeți per punct de cod chiar și pentru ASCII. Aceeași informație, alt transport.
  • latin-1 refuză ș la codificare: punctul de cod 537 nu încape în intervalul acoperit.
  • Decodificarea ca ASCII eșuează la aceeași poziție, din partea cealaltă.
  • Ultima linie este cea care produce bug-uri reale. latin-1 decodifică orice octet, deci cei doi octeți ai lui ș devin două caractere separate: È și un caracter de control invizibil, vizibil aici doar pentru că repr îl scrie ca \x99. Nicio excepție, doar text greșit — clasicul é din bazele de date prost configurate.

Când o codificare nu poate eșua, absența erorii nu este o confirmare că a fost aleasă corect.


Ce faci când datele sunt deja stricate

Al treilea nivel adaugă parametrul errors și distincția între pierdere și recuperare. Înlocuiește fișierul cu:

utf8 = "Iași depot".encode("utf-8")

print("replace  :", utf8.decode("ascii", errors="replace"))
print("ignore   :", utf8.decode("ascii", errors="ignore"))
print("backslash:", utf8.decode("ascii", errors="backslashreplace"))
print("replace round trips:", utf8.decode("ascii", errors="replace").encode("utf-8") == utf8)

broken = b"scan\xffLEG-8842"
try:
    broken.decode("utf-8")
except UnicodeDecodeError as exc:
    print("strict   :", exc.reason, "at position", exc.start)

escaped = broken.decode("utf-8", errors="surrogateescape")
print("surrogateescape text:", escaped.encode("unicode_escape").decode("ascii"))
print("surrogateescape round trips:", escaped.encode("utf-8", errors="surrogateescape") == broken)


replace  : Ia��i depot
ignore   : Iai depot
backslash: Ia\xc8\x99i depot
replace round trips: False
strict   : invalid start byte at position 4
surrogateescape text: scan\udcffLEG-8842
surrogateescape round trips: True


  • replace pune caracterul de înlocuire pentru fiecare octet nedecodabil, ignore îi șterge, backslashreplace îi scrie ca text vizibil. Toate trei produc un str valid și niciuna nu poate reface originalul — linia de round-trip confirmă.
  • \xff nu este un octet de start valid în UTF-8, deci decodificarea strictă eșuează. Poziția 4 este exact octetul problematic.
  • surrogateescape mapează fiecare octet nedecodabil la un punct de cod din zona U+DC80U+DCFF. Textul rezultat conține \udcff, iar recodificarea cu același handler reface octetul original — de aceea round-trip-ul reușește.

surrogateescape este handlerul folosit de Python pentru numele de fișiere din sistemul de operare, tocmai pentru că un nume de fișier trebuie să poată fi redat exact, chiar dacă nu este text valid. Pentru date care doar se afișează, replace este suficient; pentru date care se scriu înapoi, este singura alegere corectă.


Unde se face conversia în cod real

Ultimul nivel arată granițele tipice — fișier, JSON, buffer în memorie. Înlocuiește fișierul cu:

import io
import json
import pathlib

payload = {"depot": "Iași", "scans": 3}

as_text = json.dumps(payload, ensure_ascii=False)
as_bytes = as_text.encode("utf-8")

print("json text  :", as_text)
print("json bytes :", as_bytes)
print("ensure_ascii=True:", json.dumps(payload))

path = pathlib.Path("depot.json")
path.write_text(as_text, encoding="utf-8")
print("read_text  :", path.read_text(encoding="utf-8"))
print("read_bytes :", path.read_bytes())

buffer = io.BytesIO()
buffer.write(as_bytes)
print("BytesIO holds bytes:", buffer.getvalue() == as_bytes)

print("json.loads accepts both:", json.loads(as_text) == json.loads(as_bytes))
path.unlink()


json text  : {"depot": "Iași", "scans": 3}
json bytes : b'{"depot": "Ia\xc8\x99i", "scans": 3}'
ensure_ascii=True: {"depot": "Ia\u0219i", "scans": 3}
read_text  : {"depot": "Iași", "scans": 3}
read_bytes : b'{"depot": "Ia\xc8\x99i", "scans": 3}'
BytesIO holds bytes: True
json.loads accepts both: True


  • json.dumps returnează str, nu bytes — serializarea JSON este o transformare text-la-text; codificarea este un pas separat.
  • ensure_ascii=True, valoarea implicită, evită complet problema scriind ș în ASCII pur. Rezultatul este mai lung, dar trece prin orice canal care presupune ASCII.
  • write_text cu encoding explicit face codificarea în locul tău; read_bytes arată ce a ajuns efectiv pe disc.
  • json.loads acceptă și str, și bytes, detectând codificarea din primii octeți — una dintre puținele funcții din biblioteca standard care fac asta.

Tiparul vizibil aici este cel corect în general: conversia are loc la granița cu fișierul sau cu rețeaua, iar payload rămâne un dicționar cu str pe toată durata prelucrării.


Cazuri limită și capcane

  • Codificarea implicită la deschiderea unui fișier depinde de sistem. open(path) fără encoding folosește codificarea locală, care diferă între mașini. Scrie mereu encoding="utf-8" explicit.
  • b[i] este un int, dar b[i:i+1] este un bytes. Codul care iterează un bytes și compară cu b"x" nu se potrivește niciodată; compară cu ord("x") sau iterează pe felii.
  • bytearray este varianta mutabilă a lui bytes și este alegerea corectă când construiești un tampon incremental; concatenarea repetată de bytes realocă la fiecare pas.
  • str(b) nu decodifică. Produce "b'...'", reprezentarea textuală a obiectului. Python avertizează doar dacă ceri explicit, deci greșeala ajunge tăcut în loguri.
  • latin-1 este util deliberat ca transport reversibil octet-cu-octet, dar niciodată ca presupunere despre text real.
  • Comparațiile între str și bytes nu ridică excepție, returnează False. "LEG-8842" == b"LEG-8842" este fals, iar o cheie de dicționar de tipul greșit pur și simplu nu se găsește.

Rezumat

str conține puncte de cod Unicode și reprezintă text; bytes conține octeți și reprezintă transport. Conversia trece obligatoriu printr-o codificare declarată explicit, pentru că valoarea bytes nu o poartă cu sine. Cele două tipuri nu se convertesc implicit niciodată, ceea ce transformă erorile de codificare în erori de tip vizibile devreme.

  • len numără puncte de cod pe str și octeți pe bytes; b[i] returnează un int, s[i] un str.
  • Codificarea nu este stocată în date — este o convenție de protocol, iar decode fără codificare corectă produce text greșit sau o excepție.
  • latin-1 decodifică orice secvență de octeți fără să eșueze, deci absența unei erori nu confirmă alegerea corectă.
  • errors="replace" și "ignore" pierd informația definitiv; "surrogateescape" o păstrează și permite recodificarea exactă.
  • Decodifică la intrarea în program și codifică la ieșire; între ele lucrează exclusiv cu str.
  • Specifică encoding="utf-8" explicit la orice operațiune cu fișiere, pentru că valoarea implicită depinde de sistem.

Чтобы получить доступ к облачной лаборатории, необходимо войти в систему.

Войти