Un sistema de numeración que aparece en todas partes
Si llevas un tiempo programando, seguro que te has cruzado con números como 0xFF, #1E90FF, \u00F1 o cadenas larguísimas como b221d9db.... Aparecen en los colores de una hoja de estilos, en los mensajes de error de un programa que ha fallado, en las direcciones de memoria de un depurador, en los identificadores de los commits de Git, en las direcciones MAC de una tarjeta de red y en los hashes de las contraseñas. Todos tienen algo en común: están escritos en hexadecimal.
El sistema hexadecimal, o base 16, es una forma de representar números que usa dieciséis símbolos en lugar de los diez a los que estamos acostumbrados. Su gran ventaja es que encaja de forma natural con la manera en que los ordenadores almacenan la información, en bits y bytes, y al mismo tiempo es mucho más compacto y legible para las personas que el binario.
En esta guía vamos a entender qué es exactamente el sistema hexadecimal, cómo funciona el valor posicional, cómo convertir números entre decimal, binario y hexadecimal a mano y con código, y dónde se utiliza en la práctica: bytes, colores CSS, JavaScript, Python, Unicode, hashes, direcciones de red y depuración. Terminaremos con los errores más frecuentes al trabajar con valores hexadecimales.
Cómo funcionan las bases numéricas
Para entender el hexadecimal conviene repasar primero cómo funciona el sistema que usamos a diario, el decimal o base 10.
En base 10 tenemos diez dígitos, del 0 al 9. Cuando un número supera el 9, añadimos una nueva columna a la izquierda. Cada columna vale diez veces más que la de su derecha: unidades, decenas, centenas, millares. El número 2514, por ejemplo, significa:
2514 = 2 × 10³ + 5 × 10² + 1 × 10¹ + 4 × 10⁰
= 2000 + 500 + 10 + 4
Este principio, el valor posicional, se aplica a cualquier base. Lo único que cambia es cuántos dígitos hay disponibles y, por tanto, cuánto vale cada columna.
En binario, o base 2, solo existen el 0 y el 1, y cada columna vale el doble que la anterior: 1, 2, 4, 8, 16… Es el sistema que usan internamente los ordenadores, porque se corresponde directamente con los dos estados de un circuito: encendido y apagado.
En hexadecimal, o base 16, hay dieciséis dígitos, y cada columna vale dieciséis veces más que la de su derecha: 1, 16, 256, 4096, 65536…
Los dieciséis dígitos del hexadecimal
Como solo tenemos diez cifras numéricas, el sistema hexadecimal toma prestadas las seis primeras letras del alfabeto para representar los valores del 10 al 15:
| Decimal | Hexadecimal | Binario |
|---|---|---|
| 0 | 0 | 0000 |
| 1 | 1 | 0001 |
| 2 | 2 | 0010 |
| 3 | 3 | 0011 |
| 4 | 4 | 0100 |
| 5 | 5 | 0101 |
| 6 | 6 | 0110 |
| 7 | 7 | 0111 |
| 8 | 8 | 1000 |
| 9 | 9 | 1001 |
| 10 | A | 1010 |
| 11 | B | 1011 |
| 12 | C | 1100 |
| 13 | D | 1101 |
| 14 | E | 1110 |
| 15 | F | 1111 |
Las letras pueden escribirse en mayúsculas o minúsculas: FF y ff representan exactamente el mismo valor. Cada proyecto o herramienta suele preferir una forma, y lo importante es ser coherente.
Para distinguir un número hexadecimal de uno decimal, se usan prefijos o sufijos. El más común en programación es 0x, heredado del lenguaje C: 0x10 es dieciséis, mientras que 10 es diez. En CSS se usa la almohadilla #, en algunos ensambladores el sufijo h, como FFh, y en documentación matemática el subíndice 16, como en FF₁₆.
Algunos ejemplos de equivalencias:
0x1 = 1
0xA = 10
0xF = 15
0x10 = 16
0xFF = 255
0x100 = 256
0xFFF = 4095
0x1000 = 4096
0xFFFF = 65535
Fíjate en el patrón: con un dígito hexadecimal se cuentan 16 valores, del 0 al 15; con dos, 256 valores, del 0 al 255; con tres, 4096; y con cuatro, 65536.
Convertir de hexadecimal a decimal
Para convertir hexadecimal a decimal se multiplica cada dígito por la potencia de 16 que corresponde a su posición y se suman los resultados, exactamente igual que hicimos con el número decimal.
Convirtamos 0x9D2:
0x9D2 = 9 × 16² + D × 16¹ + 2 × 16⁰
= 9 × 256 + 13 × 16 + 2 × 1
= 2304 + 208 + 2
= 2514
Otro ejemplo, el valor máximo de un byte, 0xFF:
0xFF = F × 16¹ + F × 16⁰
= 15 × 16 + 15
= 240 + 15
= 255
Con un poco de práctica, los valores de dos dígitos se calculan mentalmente: el primer dígito por 16 más el segundo. 0x7B es 7 × 16 + 11, es decir, 123.
Convertir de decimal a hexadecimal
La conversión inversa se hace dividiendo repetidamente entre 16 y anotando los restos. Los restos, leídos de abajo arriba, forman el número hexadecimal.
Convirtamos 2514:
2514 ÷ 16 = 157 resto 2
157 ÷ 16 = 9 resto 13 → D
9 ÷ 16 = 0 resto 9
Leyendo los restos de abajo arriba: 9D2
Y 3000:
3000 ÷ 16 = 187 resto 8
187 ÷ 16 = 11 resto 11 → B
11 ÷ 16 = 0 resto 11 → B
Resultado: 0xBB8
Este algoritmo es fácil de implementar en cualquier lenguaje. Aquí tienes una versión en Python, útil para entender el proceso aunque en la práctica usarás las funciones integradas:
DIGITOS = "0123456789ABCDEF"
def a_hexadecimal(numero: int) -> str:
if numero == 0:
return "0"
resultado = ""
while numero > 0:
numero, resto = divmod(numero, 16)
resultado = DIGITOS[resto] + resultado
return resultado
print(a_hexadecimal(2514), a_hexadecimal(3000), a_hexadecimal(255))
9D2 BB8 FF
La relación entre hexadecimal y binario
La verdadera razón por la que el hexadecimal es tan popular en informática es su relación con el binario. Como 16 es 2 elevado a 4, cada dígito hexadecimal representa cuatro bits exactos. Esto permite convertir entre ambos sistemas sin hacer ninguna operación aritmética: basta con sustituir cada dígito por su grupo de cuatro bits, o al revés.
Hexadecimal: D E
Binario: 1101 1110
0xDE = 11011110
Para pasar de binario a hexadecimal, se agrupan los bits de cuatro en cuatro empezando por la derecha, rellenando con ceros a la izquierda si hace falta:
Binario: 10 1011 0110
Rellenado: 0010 1011 0110
Hexadecimal: 2 B 6
1010110110 = 0x2B6
Un grupo de cuatro bits se llama nibble, y dos nibbles forman un byte. Por eso un byte, que tiene ocho bits, se representa siempre con exactamente dos dígitos hexadecimales, desde 0x00 hasta 0xFF. Compara la legibilidad:
Binario: 11000000 10101000 00000000 00000001
Hexadecimal: C0 A8 00 01
Decimal: 192 168 0 1
Leer y escribir 32 bits en binario es tedioso y propenso a errores; en hexadecimal son ocho caracteres que se pueden descomponer de un vistazo en bytes. Esa combinación de compacidad y correspondencia directa con los bits es lo que convierte al hexadecimal en el idioma habitual de la programación de bajo nivel, los sistemas embebidos, los controladores y la depuración.
Hexadecimal en JavaScript
JavaScript admite literales hexadecimales con el prefijo 0x, junto con 0b para binario y 0o para octal. El valor resultante es un número normal; la notación solo cambia cómo lo escribes en el código:
const mascara = 0xff;
console.log(mascara, mascara === 255);
console.log(0x9d2); // 2514
console.log(0b11011110); // 222
console.log(0xff_ff); // 65535, los separadores _ mejoran la legibilidad
255 true
2514
222
65535
Para convertir un número a texto hexadecimal se usa toString(16), y para el camino inverso parseInt con base 16:
console.log((2514).toString(16)); // '9d2'
console.log((2514).toString(16).toUpperCase()); // '9D2'
console.log((255).toString(16).padStart(4, "0")); // '00ff'
console.log(parseInt("ff", 16)); // 255
console.log(parseInt("0x1F")); // 31, reconoce el prefijo
console.log(Number("0xFF")); // 255
Los literales con 0x solo admiten números enteros: no existe una forma de escribir 25.14 como literal hexadecimal. Sin embargo, toString(16) sí convierte números con decimales, mostrando la parte fraccionaria en base 16:
console.log((0.5).toString(16));
console.log((25.14).toString(16));
0.8
19.23d70a3d70a4
Los números de JavaScript son de coma flotante de 64 bits y solo representan enteros con exactitud hasta Number.MAX_SAFE_INTEGER, que en hexadecimal es 0x1fffffffffffff. Para valores mayores, como identificadores de 64 bits o claves criptográficas, hay que usar BigInt, que también acepta literales hexadecimales:
const maximo64 = 0xffffffffffffffffn;
console.log(maximo64);
console.log(BigInt("0x" + "f".repeat(16)).toString());
18446744073709551615n
18446744073709551615
Hexadecimal en Python
Python ofrece herramientas equivalentes. Los literales usan también el prefijo 0x, la función hex convierte a texto y int con base 16 hace la conversión inversa:
print(0x9D2) # 2514
print(hex(2514)) # '0x9d2'
print(int("9d2", 16)) # 2514
print(int("0x9D2", 0)) # 2514, base 0 detecta el prefijo
Para controlar el formato, las f-strings son la opción más flexible:
valor = 2514
print(f"{valor:x}") # minúsculas sin prefijo
print(f"{valor:X}") # mayúsculas sin prefijo
print(f"{valor:#06x}") # con prefijo y relleno hasta 6 caracteres
print(f"{255:02X}") # siempre dos dígitos, ideal para bytes
9d2
9D2
0x09d2
FF
Python también facilita trabajar con secuencias de bytes en hexadecimal, algo muy habitual al manejar datos binarios, protocolos de red o criptografía:
datos = "Hola".encode()
print(datos.hex()) # '486f6c61'
print(datos.hex(" ")) # '48 6f 6c 61'
print(bytes.fromhex("486f6c61")) # b'Hola'
Y los números de coma flotante tienen su propio método hex, que muestra su representación interna exacta:
print((0.5).hex())
0x1.0000000000000p-1
Colores hexadecimales en CSS
El uso más visible del hexadecimal en el desarrollo web son los colores. Un color en formato #RRGGBB está formado por tres bytes, uno por cada canal de color: rojo, verde y azul. Cada canal va de 00, ausencia total, a FF, intensidad máxima:
:root {
--rojo: #ff0000;
--verde: #00ff00;
--azul: #0000ff;
--blanco: #ffffff;
--negro: #000000;
--azul-intenso: #1e90ff;
--naranja: #ffa500;
}
El color #1e90ff se descompone así: rojo 1e, que es 30; verde 90, que es 144; y azul ff, que es 255. Es exactamente el mismo color que rgb(30 144 255).
Cuando los dos dígitos de cada canal son iguales, se puede usar la forma abreviada de tres dígitos: #f80 equivale a #ff8800, y #fff a #ffffff.
Durante años, la notación hexadecimal no permitía indicar transparencia, y había que recurrir a rgba(). Hoy los colores hexadecimales con transparencia están disponibles en todos los navegadores modernos mediante un cuarto par de dígitos para el canal alfa, con los formatos #RRGGBBAA y #RGBA:
.capa-modal {
background: #00000080; /* negro al 50 % de opacidad aproximadamente */
}
.resaltado {
background: #ffa50033; /* naranja con un 20 % de opacidad */
}
.borde-suave {
border-color: #0008; /* forma abreviada: negro con alfa 88 */
}
El valor alfa va de 00, totalmente transparente, a ff, totalmente opaco. 80 corresponde a 128, aproximadamente la mitad. Para calcular el valor de un porcentaje, se multiplica 255 por el porcentaje y se convierte a hexadecimal: un 20 % es 255 × 0,2 ≈ 51, que es 33.
Convertir entre colores hexadecimales y valores RGB es una tarea habitual en JavaScript:
function hexARgb(hex) {
const limpio = hex.replace("#", "");
return [0, 2, 4].map((i) => parseInt(limpio.slice(i, i + 2), 16));
}
function rgbAHex(r, g, b) {
return "#" + [r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("");
}
console.log(hexARgb("#1e90ff"));
console.log(rgbAHex(255, 165, 0));
[ 30, 144, 255 ]
#ffa500
Ten en cuenta que el hexadecimal no es la única ni siempre la mejor forma de expresar colores. CSS moderno ofrece funciones como rgb(), hsl(), oklch() y color-mix(), que facilitan crear variantes más claras u oscuras de un color o trabajar con espacios de color más amplios. El hexadecimal sigue siendo, sin embargo, el formato más extendido en herramientas de diseño y guías de estilo.
Unicode y codificación de caracteres
Los puntos de código de Unicode, que identifican cada carácter de cualquier idioma, se escriben tradicionalmente en hexadecimal con el prefijo U+. La letra ñ es U+00F1, la é es U+00E9 y el emoji de la cara sonriente es U+1F600.
Los lenguajes de programación permiten usar estos códigos en las cadenas:
console.log("\u00F1"); // ñ
console.log(String.fromCodePoint(0x1f600)); // 😀
console.log("é".codePointAt(0).toString(16)); // 'e9'
<p>España — café</p>
Cuando se guarda un texto en UTF-8, cada carácter se convierte en uno o varios bytes, que también se muestran habitualmente en hexadecimal. La ñ, por ejemplo, ocupa dos bytes:
console.log([...new TextEncoder().encode("Ñ")].map((b) => b.toString(16)));
[ 'c3', '91' ]
Saber leer estos valores ayuda a diagnosticar los famosos problemas de codificación en los que aparece Ñ en lugar de Ñ: son precisamente los bytes C3 91 interpretados como dos caracteres en Latin-1.
Hashes, identificadores y direcciones
Muchos valores que manejamos a diario son secuencias de bytes que se muestran en hexadecimal para que sean legibles.
Los hashes criptográficos, como SHA-256, producen 32 bytes que se representan con 64 caracteres hexadecimales:
import hashlib
print(hashlib.sha256(b"hola").hexdigest())
b221d9dbb083a7f33428d7c2a3c3198ae925614d70210e28716ccaa7cd4ddb79
Los identificadores de commit de Git son hashes en hexadecimal, que normalmente se muestran abreviados a sus primeros siete caracteres o más. Los UUID son 16 bytes escritos como 32 dígitos hexadecimales separados por guiones, como 6f1c2a7e-4b3d-4f5e-9a1b-2c3d4e5f6a7b.
En redes, las direcciones MAC son seis bytes en hexadecimal, como 3c:22:fb:9a:10:4e, y las direcciones IPv6 son ocho grupos de cuatro dígitos hexadecimales, como 2001:db8:85a3::8a2e:370:7334.
En depuración, las direcciones de memoria y los códigos de error de los sistemas operativos también aparecen en hexadecimal. Un error de Windows como 0xC0000005, por ejemplo, indica una violación de acceso a memoria.
Inspeccionar archivos en hexadecimal
Los editores y volcadores hexadecimales permiten inspeccionar archivos en hexadecimal, viendo sus bytes reales. Es muy útil para analizar formatos binarios, depurar problemas de codificación o comprobar la firma de un archivo. En Linux y macOS, herramientas como xxd, hexdump u od hacen este trabajo:
printf 'Hola\n' > saludo.txt
od -A x -t x1z saludo.txt
000000 48 6f 6c 61 0a >Hola.<
000005
La columna de la izquierda es la posición en el archivo, también en hexadecimal; después vienen los bytes y, a la derecha, su interpretación como texto. Se ve claramente que 48 es la H, 6f la o y 0a el salto de línea.
Los primeros bytes de un archivo suelen identificar su formato. Un PNG empieza siempre por 89 50 4E 47, un PDF por 25 50 44 46, que en texto es %PDF, y un ZIP por 50 4B, las iniciales PK de su creador. Estas firmas, llamadas números mágicos, permiten detectar el tipo real de un archivo aunque su extensión sea engañosa.
Operaciones con bits y máscaras
En programación de bajo nivel, el hexadecimal es la forma natural de escribir máscaras de bits, porque cada dígito corresponde a cuatro bits concretos:
const byte = 0xa3; // 1010 0011
const nibbleBajo = byte & 0x0f; // 0011 → 3
const nibbleAlto = byte >> 4; // 1010 → 10 (0xA)
console.log(nibbleBajo, nibbleAlto);
3 10
Este tipo de operaciones son habituales al trabajar con protocolos de red, formatos de imagen, registros de hardware o permisos codificados en bits. Escribir la máscara 0x0F deja claro de inmediato que se seleccionan los cuatro bits inferiores, algo mucho menos evidente con su equivalente decimal, 15.
Valores negativos en hexadecimal
Los ordenadores representan los enteros con signo mediante el complemento a dos, y al mostrar los valores negativos en hexadecimal aparecen resultados que pueden confundir.
Las funciones de conversión de los lenguajes de alto nivel suelen mostrar el signo delante:
console.log((-26).toString(16));
print(hex(-26))
-1a
-0x1a
Pero en memoria, el valor -26 almacenado en un byte es 0xE6. Para obtener esa representación hay que aplicar una máscara con el tamaño deseado:
print(hex(-26 & 0xFF)) # un byte
print(hex(-26 & 0xFFFFFFFF)) # 32 bits
0xe6
0xffffffe6
Por eso, en un depurador o en un volcado de memoria, un valor como 0xFFFFFFE6 puede corresponder tanto al entero sin signo 4294967270 como al entero con signo -26. La interpretación depende del tipo de dato.
Errores comunes al trabajar con hexadecimal
El primero es confundir el valor con su representación. 0xFF y 255 son el mismo número; el hexadecimal es solo una forma de escribirlo. Usarlo en el código no cambia la lógica del programa, solo su legibilidad.
El segundo es olvidar indicar la base al convertir texto. En JavaScript, parseInt('ff') sin base devuelve NaN, y en Python int('ff') lanza un error. Indica siempre la base 16 explícitamente:
int("ff")
ValueError: invalid literal for int() with base 10: 'ff'
El tercero es no validar la entrada. parseInt de JavaScript es permisivo y se detiene en el primer carácter no válido sin avisar, mientras que Number falla por completo:
console.log(parseInt("1G", 16)); // 1, ignora la G
console.log(Number("0x1G")); // NaN
Si procesas valores introducidos por el usuario, valídalos antes con una expresión regular como /^[0-9a-f]+$/i.
El cuarto es perder los ceros a la izquierda. (5).toString(16) devuelve '5', no '05'. Al construir colores o representar bytes, usa siempre padStart(2, '0') o el formato 02x, o el resultado será incorrecto: #5ff no es lo mismo que #05ff00.
El quinto es mezclar mayúsculas y minúsculas al comparar cadenas hexadecimales. 'FF' === 'ff' es falso como texto, aunque representen el mismo número. Normaliza a un mismo formato antes de comparar.
El sexto es superar el rango seguro de los números en JavaScript al convertir identificadores o hashes largos. Usa BigInt para valores de más de 53 bits.
Y el séptimo es confundir el orden de los bytes. Los procesadores x86 y ARM almacenan los enteros en orden little-endian, con el byte menos significativo primero, de modo que el número 0x12345678 aparece en memoria como 78 56 34 12. Tenlo en cuenta al leer volcados de memoria o formatos binarios.
Conclusiones
El sistema hexadecimal es una forma de representar números en base 16, con los dígitos del 0 al 9 y las letras de la A a la F. Funciona con el mismo principio de valor posicional que el decimal, pero cada columna vale dieciséis veces más que la anterior. Convertir entre bases es sencillo: multiplicando por potencias de 16 para pasar a decimal, y dividiendo repetidamente entre 16 para el camino inverso.
Su enorme popularidad en informática se debe a su relación con el binario: cada dígito hexadecimal equivale exactamente a cuatro bits, y dos dígitos forman un byte. Esto lo convierte en una notación compacta y legible para cualquier dato que viva a nivel de bits y bytes.
En el trabajo diario lo encontrarás en los literales 0x de JavaScript, Python y casi cualquier lenguaje, en los colores CSS, que hoy admiten también transparencia con ocho dígitos, en los puntos de código de Unicode, en los hashes, los UUID y los commits de Git, en las direcciones MAC e IPv6 y en los volcados de memoria y archivos binarios.
Dominar las funciones de conversión de tu lenguaje, cuidar los ceros a la izquierda, indicar siempre la base al convertir texto y tener presentes el complemento a dos y el orden de los bytes te evitará la mayoría de los errores. Con estos conceptos, esos códigos que antes parecían crípticos pasarán a ser información perfectamente legible.
