Skip to main content

Blockchain Explained for Python Developers

·1772 words·9 mins·
Photo by Joey Kyber on Unsplash
Blockchain is the foundational technology behind Bitcoin. It is not magic: it is a clever combination of data structures, cryptographic hashing, and distributed consensus. The best way to understand it is to build a minimal one in Python.

If you want a high-level overview before diving into code, read Bitcoin in Brief: How It Works, Why It Matters, and What Comes Next.

Warning

This tutorial implements a simplified blockchain for educational purposes. Production blockchains require Merkle trees, digital signatures, P2P networking, and careful serialization. Do not use this code for real cryptocurrency development.

What Is a Blockchain?
#

A blockchain is an append-only linked list of blocks, where each block contains:

  • A set of data (transactions, messages, anything).
  • The cryptographic hash of the previous block.
  • A timestamp and a nonce used for mining.

The key invariant is: changing any past block invalidates all subsequent blocks, because each block’s hash depends on its predecessor. This makes the chain tamper-evident without requiring a central authority.

Step 1: Cryptographic Hashing
#

Before building blocks, we need to understand the primitive that makes everything work: the cryptographic hash function.

A hash function maps arbitrary data to a fixed-size digest with three critical properties:

  • Deterministic: the same input always produces the same output.
  • One-way: given a digest, you cannot recover the input.
  • Avalanche effect: a tiny change in the input completely changes the digest.

Bitcoin block headers are hashed with double-SHA-256 in consensus rules. For tutorial simplicity, we use a single SHA-256 in examples:

import hashlib

def sha256(data: str) -> str:
    return hashlib.sha256(data.encode()).hexdigest()

print(sha256("hello"))
# => 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

print(sha256("hello!"))
# => ce06092fb948d9af6d6634baab36bdb1e8fa05bb53c98284aaf24f8282fee43c

One extra exclamation mark completely changes the output. This is the avalanche effect in action.

Step 2: The Block Structure
#

A block is a container for data, linked to the previous block through its hash.

import hashlib
import json
import time
from dataclasses import dataclass, field


@dataclass
class Block:
    index: int
    timestamp: float
    data: list[dict]
    previous_hash: str
    nonce: int = 0
    hash: str = field(default="", init=False)

    def __post_init__(self) -> None:
        if not self.hash:
            self.hash = self.compute_hash()

    def compute_hash(self) -> str:
        """
        Computes the SHA-256 hash of the block contents.
        
        ⚠️ Note: For production systems, never rely on JSON serialization 
        for cryptographic hashing due to floating-point inconsistencies or 
        formatting differences across languages. Use canonical binary 
        representations (CBOR, Protobuf) instead.
        """
        block_content = {
            "index": self.index,
            "timestamp": self.timestamp,
            "data": self.data,
            "previous_hash": self.previous_hash,
            "nonce": self.nonce,
        }
        # sort_keys=True ensures deterministic output regardless of dict insertion order
        return hashlib.sha256(
            json.dumps(block_content, sort_keys=True, separators=(',', ':')).encode()
        ).hexdigest()

    def mine(self, difficulty: int) -> None:
        target = "0" * difficulty
        while not self.hash.startswith(target):
            self.nonce += 1
            self.hash = self.compute_hash()
            # Note: In production, add progress logging or async yielding here

Notice that previous_hash is part of the hashed content. This is what chains blocks together: each block cryptographically commits to its entire history.

Step 3: The Genesis Block
#

The first block in a chain has no predecessor. By convention it references a well-known constant as its previous_hash.

Note

Bitcoin’s actual genesis block (Jan 3, 2009) contains a newspaper headline about bank bailouts and was mined by Satoshi Nakamoto. We use “0” * 64 here for tutorial simplicity.

def create_genesis_block() -> Block:
    return Block(
        index=0,
        timestamp=int(time.time()),
        data=[{
            "message": "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"
        }],
        previous_hash="0" * 64,
    )

genesis = create_genesis_block()
print(genesis.hash)
# => some sha256 digest starting with zeros after mining

Step 4: Building the Chain
#

A Blockchain class holds the list of blocks and enforces the linking rule when adding new ones.

class Blockchain:
    def __init__(self, difficulty: int = 4) -> None:
        self.difficulty = difficulty
        self.chain: list[Block] = []
        self._add_genesis()

    def _add_genesis(self) -> None:
        genesis = create_genesis_block()
        genesis.mine(self.difficulty)
        self.chain.append(genesis)

    @property
    def last_block(self) -> Block:
        return self.chain[-1]

    def add_block(self, data: list[dict]) -> Block:
        block = Block(
            index=len(self.chain),
            timestamp=int(time.time()),  # Use int() to avoid float precision issues
            data=data,
            previous_hash=self.last_block.hash,
        )
        block.mine(self.difficulty)
        self.chain.append(block)
        return block

    def is_valid(self) -> bool:
        """
        Validates the entire chain:
        1. Each block's hash matches recomputed hash
        2. Each block correctly references its predecessor
        3. Proof of Work is valid (hash starts with required zeros)
        """
        target = "0" * self.difficulty
        for i in range(1, len(self.chain)):
            cur = self.chain[i]
            prev = self.chain[i - 1]

            # Check hash integrity
            if cur.hash != cur.compute_hash():
                return False

            # Check chain linkage
            if cur.previous_hash != prev.hash:
                return False

            # Check proof of work
            if not cur.hash.startswith(target):
                return False

        return True

Let’s test the tamper-evidence property:

bc = Blockchain(difficulty=4)
bc.add_block([{"from": "alice", "to": "bob", "amount": 10}])
bc.add_block([{"from": "bob", "to": "carol", "amount": 3}])

print(bc.is_valid())  # True

# Tamper with block 1 (direct modification bypasses mining!)
bc.chain[1].data = [{"from": "alice", "to": "eve", "amount": 100}]

print(bc.is_valid())  # False — hash mismatch detected immediately

Step 5: Proof of Work
#

The chain above is tamper-evident but not tamper-resistant: an attacker can simply recompute all hashes after tampering. This is where proof of work comes in.

The idea: require that a valid block hash must start with a certain number of zeros. This is called the difficulty target. To satisfy it, a miner must increment a nonce field until the hash meets the condition. This is cheap to verify but expensive to compute.

def mine_block(block: Block, difficulty: int) -> Block:
    target = "0" * difficulty
    while not block.hash.startswith(target):
        block.nonce += 1
        block.hash = block.compute_hash()
    return block

With difficulty = 4, a valid hash must start with 0000. Because SHA-256 output is effectively random, finding such a nonce requires, on average, $16^4 = 65,536$ attempts.

To keep the example aligned with that estimate, we mine with difficulty 4 below.

import time

block = Block(
    index=1,
    timestamp=int(time.time()),
    data=[{"from": "alice", "to": "bob", "amount": 10}],
    previous_hash="0" * 64,
)

start = time.time()
block.mine(difficulty=4)
elapsed = time.time() - start

print(f"Nonce: {block.nonce}")
print(f"Hash:  {block.hash}")
print(f"Time:  {elapsed:.3f}s")

In Bitcoin, the difficulty is recalibrated every 2016 blocks (roughly two weeks) so that the average block time stays near 10 minutes regardless of how much total hash power is on the network.

Step 6: Why Proof of Work Secures History
#

Suppose an attacker wants to rewrite block 3 in a chain of 10 blocks. They must:

  1. Tamper with block 3’s data.
  2. Re-mine block 3 to find a valid nonce.
  3. Re-mine blocks 4 through 10, because each one references the hash of its predecessor.

Meanwhile, the honest network keeps extending the chain. The attacker must outpace all honest miners combined. At global Bitcoin scale, this would require more than 50% of the world’s total SHA-256 hash power: the so-called 51% attack. At current network hash rates, sustaining such an attack is extraordinarily expensive and operationally difficult.

Step 7: Distributed Consensus
#

A blockchain only achieves its security guarantees when it is distributed across many independent nodes. Each node holds a full copy of the chain and verifies every incoming block.

In practice, nodes follow the valid chain with the most accumulated proof-of-work (often approximated as the longest chain when difficulty is stable).

The consensus rule is simple: competing chains are resolved by accumulating the most cumulative difficulty. When two nodes broadcast competing valid blocks at the same time (a fork), nodes adopt whichever branch accumulates work fastest. The slower branch is abandoned. This is called the Nakamoto consensus.

  • Node A: genesis → B1 → B2 → B3 → B4 ← heaviest: canonical
  • Node B: genesis → B1 → B2 → B3’ ← abandoned after B4 arrives

No central coordinator is needed. The protocol converges through competition.

Putting It All Together
#

Here is a minimal but complete working blockchain in Python, combining all the pieces above:

import hashlib
import json
import time
from dataclasses import dataclass, field


@dataclass
class Block:
    index: int
    timestamp: int
    data: list[dict]
    previous_hash: str
    nonce: int = 0
    hash: str = field(default="", init=False)

    def __post_init__(self) -> None:
        if not self.hash:
            self.hash = self.compute_hash()

    def compute_hash(self) -> str:
        content = {
            "index": self.index,
            "timestamp": self.timestamp,
            "data": self.data,
            "previous_hash": self.previous_hash,
            "nonce": self.nonce,
        }
        return hashlib.sha256(
            json.dumps(content, sort_keys=True, separators=(',', ':')).encode()
        ).hexdigest()

    def mine(self, difficulty: int) -> None:
        target = "0" * difficulty
        while not self.hash.startswith(target):
            self.nonce += 1
            self.hash = self.compute_hash()


class Blockchain:
    def __init__(self, difficulty: int = 4) -> None:
        self.difficulty = difficulty
        self.chain: list[Block] = []
        self._add_genesis()

    def _add_genesis(self) -> None:
        genesis = Block(0, int(time.time()), [
            {"message": "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"}
        ], "0" * 64)
        genesis.mine(self.difficulty)
        self.chain.append(genesis)

    @property
    def last_block(self) -> Block:
        return self.chain[-1]

    def add_block(self, data: list[dict]) -> Block:
        block = Block(
            index=len(self.chain),
            timestamp=int(time.time()),
            data=data,
            previous_hash=self.last_block.hash,
        )
        block.mine(self.difficulty)
        self.chain.append(block)
        return block

    def is_valid(self) -> bool:
        target = "0" * self.difficulty
        for i in range(1, len(self.chain)):
            cur, prev = self.chain[i], self.chain[i - 1]
            if cur.hash != cur.compute_hash():
                return False
            if cur.previous_hash != prev.hash:
                return False
            if not cur.hash.startswith(target):
                return False
        return True


# --- Demo ---
if __name__ == "__main__":
    print("Initializing Blockchain with Difficulty 4...")
    bc = Blockchain(difficulty=4)
    
    print(f"\nGenesis Block mined. Hash: {bc.last_block.hash[:20]}...")
    
    bc.add_block([{"from": "alice", "to": "bob", "amount": 10}])
    bc.add_block([{"from": "bob", "to": "carol", "amount": 3}])
    
    print("\n--- Chain State ---")
    for block in bc.chain:
        print(f"[{block.index}] Nonce: {block.nonce:>8} | Hash: {block.hash[:20]}…")
    
    print(f"\nChain Valid: {bc.is_valid()}")

Sample output:

Initializing Blockchain with Difficulty 4...

Genesis Block mined. Hash: 0000a3f9e1b7c2d5f6...

--- Chain State ---
[0] Nonce:     14823 | Hash: 0000a3f9e1b7c2d5f6…
[1] Nonce:      8741 | Hash: 0000f8a1c93e217b49…
[2] Nonce:     31205 | Hash: 00003d7b5f2ea86c10…

Chain Valid: True

Try adjusting difficulty from 2 to 6 and observe how mining time scales exponentially ($16^n$). And how CPU burns…

Key Takeaways
#

PropertyMechanismCode Example
ImmutabilityEach block’s hash depends on its datacompute_hash()
Proof of WorkNonce mining to meet difficulty targetmine() method
Tamper evidenceEach block hashes its predecessorhashlib.sha256
Tamper resistanceMining makes rewriting expensivewhile not hash.startswith(target)
OrderingBlocks form a strict sequencelinked list via previous_hash
Distributed trustAll nodes verify; heaviest chain winsNakamoto consensus
FinalityMore confirmations → harder to rewriteexponential cost growth

What This Simplified Model Leaves Out
#

This implementation illustrates the core mechanics but omits several production-critical components found in Bitcoin:

Missing ComponentPurposeComplexity Added
Merkle treesProve transaction inclusion without downloading all transactionsRequires tree hashing logic
UTXO modelTrack unspent outputs instead of balances; prevents double-spendingComplex state management
Elliptic curve signaturesVerify transaction ownership via ECDSA/Schnorr signaturesCryptography library integration
P2P networkingNodes discover peers and propagate transactions/blocksSocket programming + protocols
Script/opcodesDefine spending conditions, multi-signature, time locksStack-based VM interpreter

Each of these is a rich topic in its own right. The blockchain data structure itself is the foundation, and as this post shows, it is surprisingly straightforward once you understand the hashing primitive it rests on.

References
#