Skip to content

Data Types

Data type is not a detail.

Data type is a contract.

It defines:

  • what a value means
  • how much space it takes
  • which operations make sense
  • what kind of bug can grow from it

When you understand data types well, you stop programming only on the surface and start understanding how the machine actually sees what you wrote.

And that changes everything.

Before talking about types: everything becomes bits

Section titled “Before talking about types: everything becomes bits”

At the end of the day, memory does not know what int, float, char, or bool means.

It only stores bits.

A bit is the smallest unit of digital information:

  • 0
  • 1

Eight bits make one byte.

1 bit = 0 or 1
8 bits = 1 byte
1024 bytes = 1 KB
1024 KB = 1 MB

Example of one byte:

01000001

That same byte can be interpreted in multiple ways:

  • as decimal number 65
  • as hexadecimal 0x41
  • as ASCII character 'A'

Notice the core idea:

the bits are the same; meaning changes based on type and context.

Data representation: memory does not “see” semantics

Section titled “Data representation: memory does not “see” semantics”

Look at this 1-byte block:

Bits: 01000001
Dec: 65
Hex: 0x41
ASCII: 'A'

Memory does not know whether this is a letter or a number.

That is decided by:

  • the type
  • the instruction being used
  • the way the value is read

That is why characters have interchangeable numeric values.

In ASCII:

  • 'A' = 65
  • 'B' = 66
  • 'a' = 97
  • '0' = 48

For the full table, see ASCII Table.

Mental diagram: same data, different interpretations

Section titled “Mental diagram: same data, different interpretations”
Address: 0x1000
+--------+
| 01000001 |
+--------+
If read as unsigned char -> 65
If read as ASCII char -> 'A'
If read as raw binary -> 01000001

This is one of the most important ideas in all of computing.

Integers represent values with no fractional part.

Classic examples:

  • age
  • quantity
  • retries
  • stock
  • position

In C, the exact size depends on the platform and compiler, but the standard guarantees minimum sizes.

char // at least 8 bits
short // at least 16 bits
int // at least 16 bits
long // at least 32 bits
long long // at least 64 bits

In C++, the base idea is similar to C:

char
short
int
long
long long

But C++ gives you safer casting tools and modern library utilities.

JavaScript is very different.

Most regular numbers are Number, which uses 64-bit IEEE 754 floating point.

That means:

  • small integers usually work well
  • decimals exist
  • very large integers lose precision
console.log(typeof 10); // "number"
console.log(typeof 10.5); // "number"

For truly large integers:

const id = 9007199254740993n;
console.log(typeof id); // "bigint"

Python abstracts more.

int in Python grows as needed.

x = 10
y = 10**100
print(type(x)) # <class 'int'>
print(type(y)) # <class 'int'>

That is great for ergonomics, but it does not mean zero cost.

Huge integers consume more memory and more processing.

In languages like C and C++, this matters a lot.

Allows negative and positive numbers.

Allows only zero and positive values.

Example with 8 bits:

unsigned 8 bits: 0 to 255
signed 8 bits: -128 to 127

How negative values are stored: two’s complement

Section titled “How negative values are stored: two’s complement”

The most common way to represent negative integers is two’s complement.

Example with 8 bits:

5 = 00000101
-5 = 11111011

How to build -5 in two’s complement:

5 in binary: 00000101
invert the bits: 11111010
add 1: 11111011

This matters because overflow, casting, and binary arithmetic depend on this representation.

#include <stdio.h>
int main(void) {
signed char a = -5;
unsigned char b = 250;
printf("a = %d\n", a);
printf("b = %u\n", b);
return 0;
}

A type has limits.

If you try to store a value beyond those limits, something breaks.

Conceptual example with unsigned char:

Maximum: 255
255 + 1 = 0 // wraparound in unsigned arithmetic

Example in C:

#include <stdio.h>
int main(void) {
unsigned char x = 255;
x = x + 1;
printf("%u\n", x); // usually 0
return 0;
}

With signed integers, overflow can be even more dangerous because undefined behavior enters the picture in C and C++.

If you want to manually convert a decimal integer to binary, divide by 2 and collect the remainders.

Example with 13:

13 / 2 = 6 remainder 1
6 / 2 = 3 remainder 0
3 / 2 = 1 remainder 1
1 / 2 = 0 remainder 1
Reading bottom to top:
13 = 1101

For fractions, multiply by 2 and observe the integer part.

Example with 0.625:

0.625 * 2 = 1.25 -> 1
0.25 * 2 = 0.5 -> 0
0.5 * 2 = 1.0 -> 1
0.625 = 0.101 in binary

So:

10.625 = 1010.101

This is where many people get hit without realizing it.

Floating-point does not represent “any decimal exactly.”

It represents binary approximations.

That is why cases like this exist:

console.log(0.1 + 0.2); // 0.30000000000000004
print(0.1 + 0.2) # 0.30000000000000004
#include <iostream>
int main() {
std::cout << (0.1 + 0.2) << '\n';
}

In IEEE 754, a 32-bit float is usually organized like this:

31 23 0
+-----------+-----------+
| sign | exponent | mantissa |
+-----------+-----------+
1 bit 8 bits 23 bits

For a 64-bit double:

63 52 0
+------------+---------------------+
| sign | exponent | mantissa |
+------------+---------------------+
1 bit 11 bits 52 bits

This explains why:

  • float comparisons can fail
  • precision is limited
  • using float for money is a trap
a = 0.1 + 0.2
b = 0.3
print(a == b) # False

Better approach:

import math
print(math.isclose(0.1 + 0.2, 0.3)) # True

In C++:

#include <cmath>
#include <iostream>
int main() {
double a = 0.1 + 0.2;
double b = 0.3;
std::cout << std::boolalpha << (std::fabs(a - b) < 1e-9) << '\n';
}

Money: use a proper decimal strategy or integer minor units

Section titled “Money: use a proper decimal strategy or integer minor units”

Practical rule:

  • $10.99 as 1099 cents
  • or a proper decimal type in your chosen stack

JavaScript example using cents:

const priceInCents = 1099;
const quantity = 3;
const total = priceInCents * quantity;
console.log(total); // 3297
console.log((total / 100).toFixed(2)); // "32.97"

Python example:

price_cents = 1099
quantity = 3
total_cents = price_cents * quantity
print(total_cents) # 3297
print(total_cents / 100) # 32.97

This matters a lot.

A character is not magic. It is also represented numerically.

In ASCII:

  • 'A' = 65
  • 'B' = 66
  • 'Z' = 90
  • 'a' = 97
  • '0' = 48

Visual example:

'A'
Decimal: 65
Hex: 0x41
Bin: 01000001

You mentioned “A = 52”, but the correct ASCII value is 65.

52 in ASCII is the character '4'.

Code: character to number and number to character

Section titled “Code: character to number and number to character”
#include <stdio.h>
int main(void) {
char c = 'A';
printf("%c\n", c); // A
printf("%d\n", c); // 65
return 0;
}
#include <iostream>
int main() {
char c = 'A';
std::cout << c << '\n';
std::cout << static_cast<int>(c) << '\n';
}
const c = 'A';
console.log(c.charCodeAt(0)); // 65
console.log(String.fromCharCode(65)); // A
print(ord('A')) # 65
print(chr(65)) # A

ASCII covers the classic 128 codes.

Great for understanding the foundation.

But modern software usually needs Unicode, because:

  • accents exist
  • emojis exist
  • many writing systems exist

ASCII is the starting point.

Unicode is the real world.

Even so, understanding ASCII is still extremely valuable because it helps you understand:

  • character comparison
  • basic ordering
  • parsing
  • byte manipulation
  • text serialization

Strings: text is not just “a bunch of letters”

Section titled “Strings: text is not just “a bunch of letters””

A string is a sequence of characters.

But the implementation varies a lot by language.

In C, a string is an array of char terminated by a null byte '\0'.

#include <stdio.h>
int main(void) {
char name[] = "Edu";
printf("%c\n", name[0]); // E
printf("%c\n", name[1]); // d
printf("%c\n", name[2]); // u
printf("%d\n", name[3]); // 0 -> '\0'
return 0;
}

Diagram:

name = "Edu"
+-----+-----+-----+------+
| 'E' | 'd' | 'u' | '\0' |
+-----+-----+-----+------+

In C++, you may use char[], const char*, or std::string.

#include <iostream>
#include <string>
int main() {
std::string name = "Edu";
std::cout << name.size() << '\n'; // 3
}

Strings in JavaScript are immutable.

const name = "Edu";
console.log(name[0]); // E
console.log(name.length); // 3

Strings in Python are also immutable.

name = "Edu"
print(name[0]) # E
print(len(name)) # 3

Boolean models binary state.

true / false
1 / 0
on / off
active / inactive

In C:

#include <stdbool.h>
#include <stdio.h>
int main(void) {
bool active = true;
printf("%d\n", active); // 1
return 0;
}

In JavaScript:

const active = true;
console.log(typeof active); // boolean

In Python:

active = True
print(type(active)) # <class 'bool'>

This is where a lot of confusion lives.

Plain C often works with null pointers:

int *ptr = NULL;

Modern C++ uses nullptr:

int* ptr = nullptr;

JavaScript has both null and undefined.

let a = null;
let b;
console.log(a); // null
console.log(b); // undefined

Python uses None.

value = None

Mental rule:

  • absence of value is not the same as zero
  • absence of value is not an empty string
  • absence of value is not false

Now we get to a critical part.

Casting is the act of converting one type into another.

It can be:

  • implicit
  • explicit
  • safe
  • dangerous

The language converts for you.

Sometimes it helps.

Sometimes it plants a bomb.

int a = 10;
double b = a; // implicit conversion from int to double
console.log("5" + 1); // "51"
console.log("5" - 1); // 4

That happens because JavaScript performs implicit coercion.

Powerful.

Also a bug factory if used carelessly.

You make the conversion intention obvious.

#include <stdio.h>
int main(void) {
double x = 3.99;
int y = (int)x;
printf("%d\n", y); // 3
return 0;
}

Prefer static_cast over C-style casts when possible:

#include <iostream>
int main() {
double x = 3.99;
int y = static_cast<int>(x);
std::cout << y << '\n'; // 3
}
console.log(Number("42")); // 42
console.log(parseInt("42", 10)); // 42
console.log(parseFloat("3.14")); // 3.14
print(int("42")) # 42
print(float("3.14")) # 3.14
print(str(42)) # "42"
double price = 19.99;
int truncated = static_cast<int>(price); // 19
#include <stdio.h>
int main(void) {
int x = 300;
unsigned char y = (unsigned char)x;
printf("%u\n", y); // may become 44
return 0;
}

Because:

300 % 256 = 44
value = "abc"
# int(value) -> ValueError
console.log(Number("abc")); // NaN

In JavaScript:

const value = Number("abc");
console.log(value); // NaN
console.log(Number.isNaN(value)); // true

That kind of case must be handled before business logic continues.

Hex shows up all the time because it maps very well to bytes.

Each hexadecimal digit represents 4 bits.

Binary: 11111111
Hexadecimal: FF
Decimal: 255

Conversion:

1111 1111
F F

Another example:

0100 0001
4 1
0x41 = 65 = 'A'

When a value occupies more than 1 byte, the storage order matters.

Example with 0x12345678:

+------+------+------+------+
| 12 | 34 | 56 | 78 |
+------+------+------+------+
+------+------+------+------+
| 78 | 56 | 34 | 12 |
+------+------+------+------+

This appears in:

  • serialization
  • networks
  • binary file reading
  • interoperability between systems

Example in C:

#include <stdio.h>
int main(void) {
printf("char: %zu\n", sizeof(char));
printf("short: %zu\n", sizeof(short));
printf("int: %zu\n", sizeof(int));
printf("long: %zu\n", sizeof(long));
printf("long long: %zu\n", sizeof(long long));
printf("float: %zu\n", sizeof(float));
printf("double: %zu\n", sizeof(double));
return 0;
}

Example in C++:

#include <iostream>
int main() {
std::cout << sizeof(char) << '\n';
std::cout << sizeof(int) << '\n';
std::cout << sizeof(double) << '\n';
}

Before choosing a type, ask:

  1. Is this quantity, identity, text, state, date, or money?
  2. Will I calculate with it?
  3. Is there a known limit?
  4. Do I need exact precision?
  5. Can the value be missing?
  6. Does this come from user input, API, or file?

Those questions prevent a lot of messy code.

name -> string
age -> integer
active -> boolean
createdAt -> date/time
orderId -> string or domain integer
amountCents -> integer
paid -> boolean
status -> enum or validated string
sku -> string
stock -> integer
priceCents -> integer

C: character and integer share the same numeric base

Section titled “C: character and integer share the same numeric base”
#include <stdio.h>
int main(void) {
char c = 'A';
int code = c;
printf("char: %c\n", c);
printf("code: %d\n", code);
return 0;
}
#include <iostream>
int main() {
double ratio = 9.87;
int whole = static_cast<int>(ratio);
std::cout << ratio << '\n';
std::cout << whole << '\n';
}
console.log("10" + 2); // "102"
console.log("10" - 2); // 8
console.log(true + 1); // 2
console.log(false + 1); // 1
raw = "42"
try:
value = int(raw)
print(value + 8)
except ValueError:
print("invalid input")
  • using strings for everything
  • comparing "10" with 10
  • using float for money
  • thinking char has no numeric value
  • converting without validation
  • mixing missing value with zero
  • ignoring the limits of a type
  • the same field changes type across layers
  • you keep converting values repeatedly
  • the system is full of defensive conditionals with no clarity
  • bugs show up in sorting, filtering, calculation, and serialization
  1. Convert 13 to binary by hand.
  2. Convert 0.625 to binary by hand.
  3. Show in code that 'A' equals 65.
  4. Model an order using cents instead of float.
  5. Perform safe string-to-integer parsing in C, C++, JavaScript, and Python.
  6. Compare 0.1 + 0.2 with 0.3 and explain the result.
  • Do you understand bits, bytes, and binary representation?
  • Do you know the difference between integer, float, char, string, and bool?
  • Do you know that a character is also a number?
  • Do you understand the basics of two’s complement?
  • Do you know why float fails for money?
  • Do you know when explicit cast is appropriate?
  • Do you validate invalid input before converting?

If you check these boxes, your foundation already moves past “tutorial level” and into real engineering.