All articles
Encoders & Decoders
28 min readBy DevUtilX Team

Text to Binary and Hex: How Computers Represent Characters

Learn how characters become code points, bytes, binary and hex: ASCII, UTF-8, hex dumps, endianness and common pitfalls, with tested JavaScript and Python code.

Text to Binary and Hex: How Computers Represent Characters

Type the word Hi! into any program and, a moment later, three tiny numbers sit in memory: 72, 105 and 33. Send it over a network and those numbers travel as electrical or optical pulses. Save it to disk and they become magnetic or electrical states. At every layer, the computer never handles letters at all. It handles numbers, and numbers are written in bits. Yet when something breaks, such as a garbled accent, a rejected packet or a file that will not open, you suddenly need to see those numbers with your own eyes.

This guide is for developers, students, security learners and anyone who has stared at a string like c3 a9 and wondered what it means. You will learn how characters become code points and then bytes, how the ASCII table is organised, why one character can take several bytes in UTF-8, and how to convert text to binary and hexadecimal by hand and in code. You will also learn how to read a hex dump, avoid the classic mistakes, and use the free Text to Binary, Binary to Text, Text to Hex and Hex to Text converters in your browser. All the code samples were run, and the outputs shown are the real ones.

From characters to numbers to bits

Three words describe the whole pipeline, and mixing them up causes most encoding bugs.

  • A character is an abstract unit of text that a human recognises, such as A, é or €.
  • A code point is the number a character set assigns to that character. In Unicode, A is U+0041, é is U+00E9 and € is U+20AC.
  • A byte is a group of eight bits, holding a value from 0 to 255. An encoding is the rule that turns code points into a sequence of bytes.

So text becomes numbers (code points), and numbers become bytes (through an encoding), and bytes are written as bits. A binary or hexadecimal view of text is simply a way of displaying those bytes.

This is why the question "what is the binary of this text?" has no single answer until you name the encoding. The letter A is 01000001 in ASCII, UTF-8 and Latin-1 alike. The letter é is one byte, 11101001, in Latin-1, but two bytes, 11000011 10101001, in UTF-8. The tools on this site use UTF-8, the encoding used by the overwhelming majority of the web, and the rest of this guide does the same unless it says otherwise.

Why binary and why hex?

Hardware stores two states, so everything is binary underneath. A single bit is too small to be useful, and long strings of ones and zeros are hard for humans to read. Hexadecimal solves that. It is base 16, using the digits 0 to 9 and the letters a to f. One hex digit represents exactly four bits (a "nibble"), so one byte is always exactly two hex digits.

Binary Hex Decimal
0000 0 0
0001 1 1
0010 2 2
0011 3 3
0100 4 4
0101 5 5
0110 6 6
0111 7 7
1000 8 8
1001 9 9
1010 a 10
1011 b 11
1100 c 12
1101 d 13
1110 e 14
1111 f 15

Memorise this sixteen-row table and you can convert between binary and hex in your head, because you never calculate. You just swap four bits for one digit. 0100 1000 is 4 and 8, so the byte is 48. That direct mapping is why programmers prefer hex to decimal when looking at raw data.

A short history of text encodings

Early telegraph and teleprinter systems used their own five-bit codes. By the late 1950s, every manufacturer's computers spoke a different dialect, and exchanging text between machines was painful. The American Standard Code for Information Interchange, ASCII, was developed in the early 1960s to fix that. It defined 128 characters using 7 bits, which was enough for English letters, digits, punctuation and a set of control codes originally designed for teleprinters.

Computers soon standardised on 8-bit bytes, which left one spare bit. Vendors used the extra 128 values (128 to 255) for their own additions: accented letters, box-drawing characters, currency symbols. Because each vendor and region chose differently, "extended ASCII" is not a single standard but a family of code pages. ISO 8859-1 (Latin-1) covers Western European languages. Windows-1252 is a close cousin that replaces some control positions with printable characters such as the euro sign. Other code pages served Cyrillic, Greek, Hebrew and more. A file written in one code page and read in another produced the familiar garbage text that developers call mojibake.

Unicode, started in the late 1980s, took a different approach: give every character in every writing system one unique code point, and treat the byte representation as a separate question. UTF-8, designed in the early 1990s, became the dominant answer to that question. It is defined in RFC 3629, and its great advantage is that the first 128 code points encode to the very same single bytes as ASCII. Any valid ASCII file is automatically a valid UTF-8 file.

If you want to go deeper on the concept of representing information with two symbols, binary code is a good overview.

The ASCII table, region by region

ASCII looks like a random list until you notice its structure. It was designed so that common operations are simple bit tricks. Here is the layout by range.

Decimal Hex Contents
0 to 31 00 to 1f Control characters
32 20 Space
33 to 47 21 to 2f Punctuation such as ! and /
48 to 57 30 to 39 Digits 0 to 9
58 to 64 3a to 40 Punctuation such as : and @
65 to 90 41 to 5a Uppercase A to Z
91 to 96 5b to 60 Punctuation such as [ and a backtick
97 to 122 61 to 7a Lowercase a to z
123 to 126 7b to 7e Punctuation such as { and ~
127 7f DEL, a control character

Control characters

Codes 0 to 31 and 127 do not print anything. They were commands for devices. The ones you still meet every day are:

Name Hex Escape in code Meaning
NUL 00 \0 Null, used as a string terminator in C
BEL 07 \a Bell
BS 08 \b Backspace
HT 09 \t Horizontal tab
LF 0a \n Line feed (Unix newline)
CR 0d \r Carriage return
ESC 1b \e or \x1b Escape, starts terminal colour sequences

Windows text files traditionally end lines with CR followed by LF (0d 0a), while Unix-like systems use LF alone (0a). That single invisible difference is behind many "my diff shows every line changed" complaints. Our guide to comparing files and diffs explains how to spot it.

Digits

The digits start at 48 (hex 30). The character 0 is 00110000, 1 is 00110001, and 9 is 00111001. The low four bits of a digit character are the digit's own value. That means converting a digit character to a number is just c & 0x0f or c - 0x30.

Uppercase and lowercase differ by one bit

Uppercase A is 65, which is 01000001. Lowercase a is 97, which is 01100001. The two differ only in the third bit from the left, the bit with value 32 (hex 20). That holds for every letter pair: B is 01000010 and b is 01100010. The designers arranged this deliberately so case conversion would not need a lookup table.

Character Decimal Binary Hex
0 48 00110000 30
9 57 00111001 39
A 65 01000001 41
Z 90 01011010 5a
a 97 01100001 61
z 122 01111010 7a

The printable range

The printable ASCII characters run from 32 (space) to 126 (~). That is 95 characters. This range is what hex dump tools show in their text column, and anything outside it is usually replaced with a dot.

Seven bits, eight-bit bytes

Every ASCII character fits in 7 bits, so in an 8-bit byte the leading bit is always 0. Look at the binary of any plain English text and you will see every byte start with a zero. When you see a byte that starts with 1 in supposedly plain text, something other than ASCII is present: a code page character or a UTF-8 multi-byte sequence.

Extended ASCII and code pages

Bytes 128 to 255 mean different things depending on the code page. Here is the same byte in three familiar contexts.

Byte ISO 8859-1 (Latin-1) Windows-1252 UTF-8
e9 é é Invalid on its own, since it starts a 3-byte sequence
80 Control character (C1) € Invalid, a continuation byte with no start
c3 Ã Ã Starts a 2-byte sequence

The consequences are practical. If a file contains the single byte e9 and you decode it as UTF-8, a strict decoder reports an error. If it contains c3 a9 and you decode it as Latin-1, you get the two characters é instead of é. We reproduce exactly that in code later in this guide.

The lesson is simple: bytes are not text until you know the encoding. When someone sends you a hex string and asks what it says, the honest answer is "it depends how it was encoded".

UTF-8 in technical depth

UTF-8 is a variable-width encoding. Each code point takes between one and four bytes, and the first byte tells you how many.

Code point range Bytes Byte pattern (x = payload bit)
U+0000 to U+007F 1 0xxxxxxx
U+0080 to U+07FF 2 110xxxxx 10xxxxxx
U+0800 to U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx
U+10000 to U+10FFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Two design features make this elegant:

  • Self-synchronising. A continuation byte always begins with 10, and a start byte never does. If you land in the middle of a stream, you can find the next character boundary by skipping bytes that begin with 10.
  • ASCII compatible. One-byte sequences start with 0, so ASCII text is unchanged.

Worked example: é

é is code point U+00E9, which is 233 in decimal and 11101001 in binary. That is above 127, so it needs two bytes. The two-byte pattern has 11 payload bits, so pad the number to 11 bits: 000 11101001. Split into 5 and 6 bits: 00011 and 101001. Add the prefixes:

  • First byte: 110 + 00011 = 11000011 = c3
  • Second byte: 10 + 101001 = 10101001 = a9

So é in UTF-8 is c3 a9, which matches the real output from our test code: 11000011 10101001.

Worked example: €

€ is U+20AC, which is 0010 0000 1010 1100 in binary. It lies between U+0800 and U+FFFF, so it uses three bytes with 16 payload bits split 4, 6 and 6: 0010, 000010, 101100.

  • 1110 + 0010 = 11100010 = e2
  • 10 + 000010 = 10000010 = 82
  • 10 + 101100 = 10101100 = ac

The result is e2 82 ac, again matching the output of the code later in this guide.

Worked example: an emoji

The grinning face emoji is U+1F600. It needs four bytes with 21 payload bits, split 3, 6, 6 and 6: 000, 011111, 011000, 000000.

  • 11110 + 000 = 11110000 = f0
  • 10 + 011111 = 10011111 = 9f
  • 10 + 011000 = 10011000 = 98
  • 10 + 000000 = 10000000 = 80

The result is f0 9f 98 80.

Characters, code points and bytes are different counts

Take the string héllo €😀. Counting it three ways in JavaScript and Python gives different answers:

What you count Result
JavaScript .length (UTF-16 code units) 9
Code points (spread in JS, len in Python) 8
UTF-8 bytes 14

The emoji is one code point but two UTF-16 code units, which is why JavaScript's .length says 9. In UTF-8, it takes four bytes. Anything that limits size, such as a database column, a protocol field or a network header, usually counts bytes, not characters.

Converting text to binary by hand

The procedure is mechanical. Take the text Hi!.

  1. Look up each character's code point. H is 72, i is 105 and ! is 33.
  2. Encode to bytes. All three are ASCII, so each is one byte with the same value.
  3. Convert each byte to binary. Repeatedly subtract the largest power of two that fits, or divide by two and read the remainders backwards.
  4. Pad to 8 bits. Add leading zeros until each group is eight bits.

For H (72): 72 = 64 + 8, so the bits for 64 and 8 are set. The eight bit positions are 128, 64, 32, 16, 8, 4, 2, 1, which gives 01001000. For i (105): 105 = 64 + 32 + 8 + 1, which gives 01101001. For ! (33): 33 = 32 + 1, which gives 00100001.

The result is 01001000 01101001 00100001, exactly what our verified JavaScript and Python functions print.

Converting text to hex by hand

Hex is shorter because each byte is two digits. For each byte, divide by 16. The quotient is the first digit and the remainder is the second.

  • H: 72 / 16 = 4 remainder 8, so 48.
  • i: 105 / 16 = 6 remainder 9, so 69.
  • !: 33 / 16 = 2 remainder 1, so 21.

The result is 48 69 21. Alternatively, take the binary you already have and split it into nibbles: 0100 1000 is 4 8. Both paths agree, which is a good habit to use as a check.

Reversing the process

Going from hex back to text means reading pairs of digits as bytes, then decoding the byte sequence with the right encoding. Take 48 65 6c 6c 6f:

  • 48 = 4 x 16 + 8 = 72 = H
  • 65 = 6 x 16 + 5 = 101 = e
  • 6c = 6 x 16 + 12 = 108 = l
  • 6c = 108 = l
  • 6f = 6 x 16 + 15 = 111 = o

The text is Hello. For binary, split into groups of eight bits starting from the left, convert each group, and decode. The two groups 01001000 01101001 become 72 and 105, so Hi.

The step people forget is the final decode. For ASCII it is trivial, but when the bytes include c3 a9, you must decode the pair together as one UTF-8 sequence to get é. Decoding byte by byte as if each were a character produces é.

Separators, prefixes and padding

Different tools and languages write the same bytes in different styles. Recognising them saves time.

Style Example for Hi Where you see it
Space separated 48 69 Hex editors, converters, documentation
Continuous 4869 Hashes, database dumps, JSON fields
0x prefix 0x48 0x69 or 0x4869 C, Java, JavaScript, Python numeric literals
\x escape \x48\x69 C, Python, JavaScript, shell strings
% escape %48%69 URL percent-encoding
U+ notation U+0048 U+0069 Unicode code points (not bytes)
Comma and 0x 0x48, 0x69 C and C++ array initialisers

Notice that U+ notation names code points, not bytes. U+00E9 is é, while its UTF-8 bytes are c3 a9. Confusing the two is a very common mistake.

Why padding to eight bits matters

The number 72 is 1001000 in binary, which is seven digits. If you write it without the leading zero, the byte boundary disappears when several bytes are joined: 1001000 1101001 cannot be unambiguously split back into bytes without the padding. Always pad each byte to eight bits and, in hex, to two digits. A byte with value 10 is 0a, not a. The sample output of our code uses padStart(8, "0") in JavaScript and :08b in Python for exactly this reason.

Endianness: when byte order matters and when it does not

Endianness describes the order in which the bytes of a multi-byte number are stored. Take the 32-bit number 0x12345678.

  • Big-endian stores the most significant byte first: 12 34 56 78.
  • Little-endian stores the least significant byte first: 78 56 34 12.

Our JavaScript test wrote that value with DataView.setUint32 both ways and printed 12345678 for big-endian and 78563412 for little-endian. Python's int.from_bytes(bytes.fromhex("12345678"), "big") returns 305419896, while reading the same bytes as little-endian gives 2018915346. Network protocols traditionally use big-endian (often called network byte order), while most common desktop and phone processors use little-endian in memory.

Why UTF-8 text has no endianness problem

UTF-8 is defined as a sequence of single bytes in a fixed order. A multi-byte character such as é is always c3 followed by a9, on every machine. There is no byte-order question because the unit of the encoding is the byte itself.

The situation differs for UTF-16 and UTF-32, whose units are 16 or 32 bits. The letter é in UTF-16 is e9 00 in little-endian and 00 e9 in big-endian, as the Python test showed. That is why UTF-16 files may begin with a byte order mark: Python's plain "A".encode("utf-16") produced ff fe 41 00, where ff fe is the mark signalling little-endian order on that machine. If you see ff fe or fe ff at the start of a file, you are looking at UTF-16.

Reading a hex dump

A hex dump shows the raw bytes of a file or buffer in three columns: an offset, the bytes in hex, and the printable characters. This is the output of a small function we wrote (shown in full below), run on the text Hello, wörld! 你好, a newline, a tab and Tab:

00000000  48 65 6c 6c 6f 2c 20 77 c3 b6 72 6c 64 21 20 e4  |Hello, w..rld! .|
00000010  bd a0 e5 a5 bd 0a 09 54 61 62                    |.......Tab|

Here is how to read it.

  • The offset column (00000000, 00000010) is the position of the first byte on that row, in hex. The second row starts at offset 0x10, which is 16, because each row holds 16 bytes.
  • The hex column shows each byte as two digits. 48 65 6c 6c 6f is Hello.
  • The ASCII column (between the pipes) shows the printable character for each byte, or a dot for anything outside 0x20 to 0x7e.

Now look closely. The ö was encoded as c3 b6, two bytes, and both show up as dots in the right-hand column because neither is printable ASCII. Likewise the Chinese characters 你好 are six bytes, e4 bd a0 e5 a5 bd, and appear as dots. The 0a and 09 are the newline and tab, also shown as dots. So the right-hand column undercounts: ten dots tell you there is non-ASCII or control data nearby, but not which.

Useful habits when reading a dump:

  1. Find the known landmarks first, such as readable strings and the 4-byte patterns that file formats use at the start (their "magic numbers").
  2. Look for 00 bytes. Runs of zeros often mean padding, UTF-16 text or fixed-size fields.
  3. Look for bytes starting with c2 to f4. In UTF-8 text those start multi-byte characters.
  4. Check the offset to see whether a row is full. A short last row just means the data ended.

Bit manipulation tricks with text

Because ASCII was designed around bit patterns, a few one-liners are worth knowing.

Toggle case with XOR 0x20. Since upper and lower case differ only in the 32 bit, XOR with 0x20 flips between them. Our test produced A from a, and flipping every letter of Hello gave hELLO. This works only for letters. XOR 0x20 on a digit or punctuation character turns it into something else, and it is not Unicode-aware, so do not use it for é or ß.

Force lower case with OR 0x20, upper case with AND 0xDF. c | 0x20 lower-cases an ASCII letter, and c & 0xDF upper-cases it. Again, apply these only after checking that the byte is a letter.

Convert a digit character to its value. '7' & 0x0f is 7.

Check for ASCII. A byte is plain ASCII when (b & 0x80) == 0. A whole buffer is ASCII when none of its bytes has the top bit set.

Find UTF-8 continuation bytes. (b & 0xC0) == 0x80 marks a continuation byte, so counting bytes where this is false gives the number of code points in valid UTF-8.

Checksums at a glance

Once you can see bytes, a natural next question is whether they arrived intact. A checksum summarises a byte sequence in a small number. These checks are for detecting accidents, not attackers.

  • Simple sum. Add all bytes and keep the low 8 bits. For Hello (48 65 6c 6c 6f) the sum is f4, which our JavaScript run confirmed.
  • XOR checksum. XOR all bytes together. Hello gives 42. This is used in some simple serial protocols, such as NMEA sentences from GPS receivers.
  • CRC32. A cyclic redundancy check detects burst errors much better than a sum. Python's zlib.crc32(b"Hello") returned 0xf7d18982, and b"hello" returned 0x3610a686. Note how changing one bit of one letter changes the entire value.

A simple sum or XOR cannot tell ab from ba, because both operations are order-independent. A CRC does catch that. For protection against deliberate tampering you need a cryptographic hash or a signature, not a checksum.

Real-world use cases

Debugging network protocols. When a client and server disagree, the truth is in the bytes on the wire. A hex view shows whether a length field is correct, whether text was sent in the encoding you expected, and whether a stray 0d 0a or 00 is in the wrong place. HTTP headers use CR LF line endings, so seeing 0d 0a in a captured stream is normal.

Inspecting files. The first bytes of many file formats identify them. A PNG image begins with the bytes 89 50 4e 47, and the 50 4e 47 spells PNG in ASCII. A ZIP archive starts with 50 4b, the letters PK. If a file will not open, a hex view of its first bytes often tells you what it really is, or that a text editor has damaged it.

Finding encoding damage. When a database or web page shows é where é belongs, converting the stored text to hex reveals the story. If you see c3 83 c2 a9, the text was encoded to UTF-8, wrongly decoded as Latin-1, and encoded to UTF-8 again, a double-encoding bug.

Encoding puzzles and CTF learning. Capture-the-flag exercises and puzzle games often hide messages as binary or hex strings. Recognising that 8-bit groups starting with 01 are probably ASCII letters, or that hex pairs between 20 and 7e are probably printable text, solves many beginner challenges quickly.

Embedded systems and IoT. Microcontrollers talk over serial lines, I2C and radio links in raw bytes. Firmware logs, sensor frames and configuration registers are routinely documented in hex, and bits within a byte often have individual meanings, such as flags. Reading them as binary is how you check which flags are set.

Teaching and learning. Seeing A turn into 01000001 makes abstract ideas about computers concrete. Students can predict the output, convert by hand, and check against a tool.

Writing escaped literals. Developers often need the \x or % form of a string, such as for a regular expression, a URL query value or a C string. The hex converter gives the bytes to wrap. If you are assembling URLs or JSON strings, our JSON Formatting Guide is a useful companion for what must be escaped there.

Common mistakes and pitfalls

Confusing character count with byte count

"héllo €😀" has 8 code points but 14 UTF-8 bytes, and JavaScript reports its length as 9. When you truncate a string to fit a limit, work in bytes and take care not to cut in the middle of a multi-byte sequence, or you will create invalid UTF-8.

Dropping leading zeros

Writing 1001000 instead of 01001000, or a instead of 0a, breaks byte alignment. Tools that join bytes without separators depend on padding. Always pad.

Mixing up UTF-8 and Latin-1

Decoding c3 a9 as Latin-1 yields é. Encoding é as Latin-1 yields the single byte e9, which is invalid on its own in UTF-8. If your converter output does not match what another system produced, compare the encodings before suspecting the bytes.

Odd-length hex strings

Each byte needs two digits, so a valid hex string has an even number of digits. An odd length usually means a lost leading zero (f instead of 0f), a truncated copy-paste or a stray character. Our test function rejected 486 with the message "odd number of hex digits: 3". Do not guess where to add the missing digit: find the source of the damage.

Treating hex digits as case-sensitive

4A and 4a are the same byte. Conventions differ, though: many tools output lower case while some documents use upper case. Compare numerically or normalise the case before comparing strings.

Forgetting prefixes and separators

If you paste 0x48 0x69 into something that expects bare hex, the x is an invalid character. If you paste \x48\x69, the backslashes are. Strip prefixes before decoding, or use a tool that handles them.

Ignoring invalid input

Real-world input contains surprises: a binary string whose length is not a multiple of eight, hex that includes the letter g, or bytes that are not valid UTF-8. A robust converter reports the problem rather than silently producing wrong text. In the tests, fromBinary("0100100") raised "length 7 is not a multiple of 8", fromHex("zz") raised a non-hex error, and strict decoding of ff fe failed because those bytes cannot occur in valid UTF-8.

Mistaking code points for bytes

U+20AC is not the bytes 20 ac. The UTF-8 bytes for € are e2 82 ac. Only for U+0000 to U+007F do the code point value and the byte value coincide.

Treating a checksum as security

A sum, XOR or CRC detects accidental corruption only. Anyone can craft data with a chosen checksum.

Step by step with the DevUtilX converters

All four tools run in your browser. Your text is processed on your own device and is not uploaded to a server, which matters when you are looking at logs or data you would rather not share. Even so, treat anything secret with care and avoid pasting passwords or API keys into any website.

Text to binary

  1. Open Text to Binary.
  2. Type or paste your text, for example Hi!.
  3. Read the output: 01001000 01101001 00100001. Each group of eight bits is one byte.
  4. Try a non-ASCII character such as é and notice that one character produces two groups, 11000011 10101001.
  5. Copy the result with the copy button.

Binary to text

  1. Open Binary to Text.
  2. Paste groups of eight bits, such as 01001000 01101001.
  3. Read the decoded text, Hi.
  4. If the output is empty or an error appears, check that the total number of bits is a multiple of eight, and that only 0 and 1 are present.

Text to hex

  1. Open Text to Hex.
  2. Enter your text. Hi! becomes 48 69 21.
  3. Try €, which becomes e2 82 ac, three bytes for one character.
  4. Copy the hex and wrap it with 0x or \x yourself if your code needs that style.

Hex to text

  1. Open Hex to Text.
  2. Paste a hex string, for example 48 65 6c 6c 6f.
  3. Read the decoded text, Hello.
  4. If you get replacement characters or an error, the bytes may not be UTF-8, or the string may have an odd number of digits.

A good verification habit is a round trip: convert text to hex, paste the hex into the reverse tool, and confirm you get the original back. If you do, the conversion pair is consistent.

Code examples you can run

Every sample below was executed in a Node and Python sandbox, and the output shown is what was printed.

JavaScript: converters and a hexdump

const enc = new TextEncoder();
const dec = new TextDecoder("utf-8", { fatal: true });

const toBinary = (s, sep = " ") =>
  [...enc.encode(s)].map((b) => b.toString(2).padStart(8, "0")).join(sep);

const toHex = (s, sep = " ") =>
  [...enc.encode(s)].map((b) => b.toString(16).padStart(2, "0")).join(sep);

function fromBinary(str) {
  const bits = str.replace(/[^01]/g, "");
  if (bits.length % 8) throw new Error("length " + bits.length + " is not a multiple of 8");
  const bytes = new Uint8Array(bits.length / 8);
  for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(bits.slice(i * 8, i * 8 + 8), 2);
  return dec.decode(bytes);
}

function fromHex(str) {
  const h = str.replace(/0x|\\x|%|\s|,/gi, "");
  if (!/^[0-9a-f]*$/i.test(h)) throw new Error("non-hex character");
  if (h.length % 2) throw new Error("odd number of hex digits: " + h.length);
  const bytes = new Uint8Array(h.length / 2);
  for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
  return dec.decode(bytes);
}

console.log(toBinary("Hi!"));
console.log(toHex("Hi!"));
console.log(toBinary("é"), toHex("é"));
console.log(toHex("€"), toHex("😀"));
console.log(fromBinary("01001000 01101001"), fromHex("48 65 6c 6c 6f"), fromHex("0x48,0x69"));

Output:

01001000 01101001 00100001
48 69 21
11000011 10101001 c3 a9
e2 82 ac f0 9f 98 80
Hi Hello Hi

The TextEncoder always produces UTF-8, and the TextDecoder is created with fatal: true, so invalid bytes raise an error rather than silently becoming the replacement character. Inputs like 486, 0100100, zz and ff fe each failed with a clear message in our run:

Error: odd number of hex digits: 3
Error: length 7 is not a multiple of 8
Error: non-hex character
Error: The encoded data was not valid for encoding utf-8

Here is the hexdump function that produced the dump shown earlier:

function hexdump(bytes, width = 16) {
  const out = [];
  for (let o = 0; o < bytes.length; o += width) {
    const ch = bytes.slice(o, o + width);
    const hex = [...ch].map((b) => b.toString(16).padStart(2, "0")).join(" ").padEnd(width * 3 - 1, " ");
    const asc = [...ch].map((b) => (b >= 0x20 && b < 0x7f ? String.fromCharCode(b) : ".")).join("");
    out.push(o.toString(16).padStart(8, "0") + "  " + hex + "  |" + asc + "|");
  }
  return out.join("\n");
}

console.log(hexdump(enc.encode("Hello, wörld! 你好\n\tTab")));

Counting three ways, and flipping case

const s = "héllo €😀";
console.log(s.length, [...s].length, enc.encode(s).length); // 9 8 14

console.log("Hello".split("").map((c) => String.fromCharCode(c.charCodeAt(0) ^ 0x20)).join(""));
// hELLO

Endianness in JavaScript

const buf = new ArrayBuffer(4);
new DataView(buf).setUint32(0, 0x12345678, false); // big-endian
console.log(Buffer.from(buf).toString("hex"));     // 12345678
new DataView(buf).setUint32(0, 0x12345678, true);  // little-endian
console.log(Buffer.from(buf).toString("hex"));     // 78563412

Python: the same converters

def to_binary(s, sep=" "):
    return sep.join(f"{b:08b}" for b in s.encode("utf-8"))

def to_hex(s, sep=" "):
    return sep.join(f"{b:02x}" for b in s.encode("utf-8"))

def from_hex(h):
    h = "".join(h.split())
    if len(h) % 2:
        raise ValueError(f"odd number of hex digits: {len(h)}")
    return bytes(int(h[i:i+2], 16) for i in range(0, len(h), 2)).decode("utf-8")

def from_binary(b):
    b = "".join(b.split())
    if len(b) % 8:
        raise ValueError(f"length {len(b)} is not a multiple of 8")
    return bytes(int(b[i:i+8], 2) for i in range(0, len(b), 8)).decode("utf-8")

print(to_binary("Hi!"))
print(to_hex("é€😀"))

Output:

01001000 01101001 00100001
c3 a9 e2 82 ac f0 9f 98 80

Verifying a hand-rolled converter against built-ins

Writing your own converter is a fine exercise, but verify it. Python's str.encode, bytes.hex and bytes.fromhex are the reference:

for t in ["héllo", "日本語", "😀", ""]:
    assert from_hex(to_hex(t)) == t
    assert from_binary(to_binary(t)) == t
    assert to_hex(t, "") == t.encode().hex()
print("ok")

This printed ok. The JavaScript version did the same check against Node's Buffer.from(t, "utf8").toString("hex") for four test strings, including the empty one, and reported roundtrip true. Round-trip tests catch a lot, but pair them with comparison against a trusted implementation, as here, because a symmetrical bug (wrong in both directions) survives a round trip.

Python: encodings, mojibake and checksums

print("é".encode("latin-1").hex(), "é".encode("cp1252").hex(), "€".encode("cp1252").hex())
print(b"\xc3\xa9".decode("latin-1"))
print("é".encode("utf-16-le").hex(), "é".encode("utf-16-be").hex(), "A".encode("utf-16").hex())

import zlib
print(hex(zlib.crc32(b"Hello")), hex(zlib.crc32(b"hello")))
print(int.from_bytes(bytes.fromhex("12345678"), "big"),
      int.from_bytes(bytes.fromhex("12345678"), "little"))

Output:

e9 e9 80
é
e900 00e9 fffe4100
0xf7d18982 0x3610a686
305419896 2018915346

The third line shows that Latin-1 and Windows-1252 agree on é (e9) but Windows-1252 also has a byte for the euro sign (80), the second line is mojibake, and the fourth line shows that the same two-byte é flips its byte order between UTF-16 little-endian and big-endian.

Performance, security and accessibility

Performance

Hex doubles the size of the data, since each byte becomes two characters, and binary text multiplies it by eight, or nine with separators. A 1 MB file becomes about 2 MB as hex and about 8 MB as bit characters. That is acceptable for debugging, but it is not a storage format. For transferring binary data in text channels, Base64 is more compact, expanding by about a third. When you convert large inputs in code, build the output in an array and join once rather than concatenating strings in a loop, and use typed arrays or Buffer for bytes rather than arrays of numbers. If you want to see how text size interacts with transfer cost, our post on JSON minification and payload size works through the same trade-offs for APIs.

Security

Converting to hex or binary is encoding, not encryption. It hides nothing, because anyone can reverse it in seconds. Never use it to "obscure" passwords or tokens.

Be careful with input validation. When code accepts hex from users, check the length and the character set before decoding, and set a size limit so one request cannot allocate gigabytes. When decoding bytes to text, be aware of security issues around invalid and overlong UTF-8 sequences. The standard forbids encodings that use more bytes than needed (for example c0 af for a slash), and decoders must reject them. Strict decoders such as TextDecoder with fatal: true do. Lenient code that accepts them has historically been a source of path-traversal and filter-bypass bugs.

Also remember the privacy point from the tool section: our converters run locally in your browser, but you should still avoid pasting secrets into any online page.

Accessibility

Long strings of ones and zeros are hard for screen reader users and for anyone with a cognitive or visual difficulty to read. If you publish binary or hex in documentation, group it into bytes with spaces, give the plain-text meaning alongside it, and avoid relying on colour alone to mark fields. In your own interfaces, use a monospaced font, adequate contrast and line spacing, and let users copy the value with a button rather than selecting carefully. Provide a text explanation of what a dump shows instead of expecting readers to decode it by sight.

Troubleshooting

Symptom Likely cause Fix
Output shows é instead of é UTF-8 bytes were decoded as Latin-1 or Windows-1252 Decode with UTF-8. If the damage is already stored, re-decode as Latin-1 to get bytes, then as UTF-8
Output shows c3 83 c2 a9 for é Double encoding: UTF-8 bytes were treated as Latin-1 text and encoded again Fix the step that mis-decodes, then repair data by reversing the extra encode
Hex to text fails or shows odd characters The string has an odd number of digits, or non-hex characters such as 0x or commas Strip prefixes and separators, and restore the missing leading zero from the source
Binary to text returns nothing or errors Bit count is not a multiple of eight, or leading zeros were dropped Pad each byte to eight bits and recount
Replacement character appears in decoded text The bytes are not valid UTF-8, or the data was cut in the middle of a character Check the source encoding and make sure you have the full byte sequence
One character gives several bytes That is normal for non-ASCII characters in UTF-8 Count bytes, not characters, when sizing fields
Length limit rejects text that looks short The limit is in bytes and the text contains multi-byte characters Measure UTF-8 length with a TextEncoder or len(s.encode())
Two systems give different hex for the same text They use different encodings (UTF-8 versus UTF-16 or a code page) Agree on UTF-8, or specify the encoding explicitly
Numbers read from a binary file are wrong Byte order mismatch between writer and reader Check the format specification for big-endian or little-endian and read accordingly
File begins with ff fe or fe ff A UTF-16 byte order mark Decode as UTF-16 with the matching order
Hex dump text column is full of dots The bytes are not printable ASCII, so the data is binary, UTF-16 or non-Latin text Decode with the right encoding instead of relying on the ASCII column
Case flipping breaks some characters XOR 0x20 was applied to non-letters or non-ASCII text Check the byte is in a-z or A-Z first, or use proper Unicode case functions

Best practices checklist

  • Name the encoding every time. Use UTF-8 unless a protocol or format says otherwise.
  • Remember that text is characters, code points and bytes, and count the right one for the job.
  • Pad binary to eight bits per byte and hex to two digits per byte.
  • Choose one hex style and document it: separators, case and prefix.
  • Validate input before decoding: even digit count, correct character set, bit count divisible by eight.
  • Use a strict decoder so invalid bytes produce errors instead of silent damage.
  • Do not cut strings in the middle of a multi-byte sequence when truncating by bytes.
  • Specify byte order for any multi-byte number in a protocol, and remember that UTF-8 text itself has none.
  • Verify custom converters against a trusted implementation, and test with é, €, an emoji, an empty string and invalid input.
  • Use checksums to detect accidents and cryptographic tools to resist attackers.
  • Never mistake encoding for encryption.
  • When debugging, look at the bytes first and the text second.

Comparison of ways to represent bytes as text

Representation Size for N bytes Readability Typical use
Binary (8-bit groups) 8N characters, plus separators Good for bit flags, poor for long data Teaching, flag registers, puzzles
Hexadecimal 2N characters Compact and byte-aligned Hex dumps, hashes, debugging, colour codes
Decimal byte values Up to 3N characters, plus separators Readable for small values Some APIs and byte arrays in docs
Octal About 3N characters Rarely used today Unix file permissions, older systems
Percent-encoding 3 characters per encoded byte Readable for mostly-ASCII text URLs and form data
Base64 About 4N/3 characters Not human readable Embedding binary in text formats, email, data URIs
Escape sequences (\x41) 4 characters per byte Fine for single characters Source code string literals

Hex strikes the best balance for human inspection, which is why every debugger and packet analyser uses it. Base64 wins on compactness when the reader is a machine.

FAQ

What is the binary of the letter A?
A is code point 65, which is 01000001 as a byte, or 41 in hex. This is the same in ASCII and UTF-8.

Why does one character sometimes give several bytes?
UTF-8 uses one byte for ASCII characters and two to four bytes for everything else. For example, é is c3 a9 and € is e2 82 ac. The leading bits of the first byte say how many bytes belong to the character.

What is the difference between ASCII and UTF-8?
ASCII defines 128 characters in 7 bits. UTF-8 can encode every Unicode character with one to four bytes, and its first 128 values are identical to ASCII, so any ASCII text is valid UTF-8.

Why are there eight bits in each group?
Because the byte, the basic unit of memory, has eight bits. Text converters show one byte per group, padded with leading zeros so the boundaries are clear.

What does 0x mean?
It is a prefix used in many programming languages to say that the digits that follow are hexadecimal. 0x41 is 65. It is notation for the reader or compiler, not part of the value.

How do I convert hex to text manually?
Split the hex into pairs, convert each pair to a decimal byte (the first digit times 16 plus the second), and look each byte up in the ASCII table for plain English. If the bytes include values above 127, decode the whole sequence as UTF-8 rather than one byte at a time.

Why did my text turn into é?
The bytes of a UTF-8 é (c3 a9) were decoded as Latin-1 or Windows-1252, where c3 is à and a9 is ©. Decode the bytes as UTF-8 instead.

Does endianness affect text?
Not for UTF-8, because its unit is the byte and the order is fixed. It does affect UTF-16 and UTF-32, and every multi-byte number such as a length field or integer, so formats must specify it.

Why must a hex string have an even number of digits?
Each byte needs exactly two hex digits. An odd count means a digit is missing, usually a leading zero that was dropped.

Is hex or binary encoding secure?
No. It is a reversible change of notation, not encryption. Anyone can convert it back, so never use it to protect secrets.

Are my inputs sent to a server when I use the converters?
No. The DevUtilX tools run in your browser, so conversion happens on your device. Still, avoid pasting passwords or private keys into any website.

How can I toggle letter case with bits?
XOR the ASCII byte with 0x20. It flips A to a and back. It works only for letters in the basic ASCII range.

Conclusion

Text on a computer is a stack of simple ideas: characters map to code points, an encoding maps code points to bytes, and bytes are displayed as bits or hex. ASCII gives that stack a tidy foundation, with controls, digits and letters arranged so that common tricks are single bit operations. UTF-8 extends the same foundation to every writing system while leaving ASCII untouched. Most real bugs come from confusing the layers, such as counting characters when a limit counts bytes, decoding with the wrong encoding, or losing a leading zero.

Try it yourself with the Text to Binary, Binary to Text, Text to Hex and Hex to Text converters, which run entirely in your browser. For related reading, see JSON Minification and Payload Size for what bytes cost on the wire, and Code Diff and Comparing Files for tracking down invisible differences such as line endings.

Further reading

Try the tools

Further reading

Related articles