A blockchain is one of those ideas that sounds intimidating until you actually write one. Strip away the cryptocurrency hype and what you have is a linked list where each node stores a cryptographic fingerprint of the one before it. Change any record, and every fingerprint downstream breaks. Python gives you everything you need to build that structure in under 100 lines of code.
This tutorial requires no prior blockchain knowledge. You need a basic understanding of Python classes and methods. By the end, you will have a working blockchain that can store data, chain blocks together cryptographically, enforce proof of work, and detect tampering.
What a Blockchain Actually Is
Picture a traditional spreadsheet row. You can open it, change a number, and save it. Nobody outside the file knows you made a change. A blockchain is built to prevent exactly that. Each block in the chain contains data, a timestamp, and a hash — a fixed-length string produced by running the block's contents through a mathematical function. That hash is then stored inside the next block as its previous_hash.
If you alter block 3, its hash changes. Block 4 stores the old hash of block 3 as its previous_hash. Now block 4's recorded previous_hash no longer matches the actual hash of block 3. The chain is broken. You would need to recompute every block from 3 onward — and, in real networks, outpace thousands of other computers doing the same thing simultaneously. That is where proof of work comes in.
Python's standard library includes hashlib, which exposes SHA-256 and other cryptographic hash functions. SHA-256 takes any input and returns a deterministic 64-character hexadecimal string. The same input always produces the same output; any change to the input produces a completely different output. No installation required — import hashlib is all you need.
The three core properties
Before writing any code, it helps to have the three structural properties of a blockchain clear in your head.
- What it means
- Once a block is added to the chain, altering its data invalidates its hash and breaks every subsequent block's
previous_hashreference. - How Python enforces it
- The
compute_hashmethod re-derives the hash on demand. Theis_validmethod compares stored hashes against freshly computed ones.
- What it means
- Each block stores the hash of the previous block, creating a linked sequence. The first block (the genesis block) has no predecessor, so its
previous_hashis set to a string of zeros. - How Python enforces it
- The
Blockchain.add_blockmethod readsself.chain[-1].hashand passes it to the newBlockconstructor.
- What it means
- Adding a block requires finding a nonce — an integer — such that the resulting hash starts with a fixed number of leading zeros. Higher difficulty means more leading zeros and more computation.
- How Python enforces it
- The
mine_blockmethod loops, incrementingself.nonceand recomputing the hash each iteration until the hash starts with the required number of zero characters.
Building the Block Class
Start with the Block class. Each block needs an index, a timestamp, some data, the hash of the previous block, a nonce for proof of work, and its own hash. Import hashlib and datetime at the top of your file.
import hashlib
import datetime
class Block:
def __init__(self, index, data, previous_hash):
self.index = index
self.timestamp = str(datetime.datetime.utcnow())
self.data = data
self.previous_hash = previous_hash
self.nonce = 0
self.hash = self.compute_hash()
def compute_hash(self):
block_string = (
str(self.index)
+ self.timestamp
+ str(self.data)
+ self.previous_hash
+ str(self.nonce)
)
return hashlib.sha256(block_string.encode()).hexdigest()
The compute_hash method concatenates every field into a single string, encodes it to bytes, and passes it through SHA-256. The result is a 64-character hex string like 00002f4a.... Notice that self.hash is assigned in __init__ right after the nonce is set to zero. Once proof of work runs, the nonce will change, so the hash gets recomputed then too.
Always include the nonce in the string you hash. If you forget it, the proof-of-work loop will compute the same hash forever because nothing in the input ever changes.
Adding proof of work
Proof of work is a mine_block method on Block. It accepts a difficulty integer and keeps incrementing the nonce until the hash starts with that many zeros.
def mine_block(self, difficulty):
target = "0" * difficulty
while not self.hash.startswith(target):
self.nonce += 1
self.hash = self.compute_hash()
print(f"Block {self.index} mined. Nonce: {self.nonce} Hash: {self.hash}")
At difficulty 4, the miner hunts for a hash that starts with 0000. On average, that takes thousands of iterations. Increase difficulty to 5 and you are looking for 00000, which multiplies the expected work by 16. For this tutorial, keep difficulty at 2 or 3 so the code runs quickly.
Build the correct line that computes a SHA-256 hash from a string variable called block_string and returns its hex digest:
return hashlib.sha256(block_string.encode()).hexdigest(). You must first encode the string to bytes before passing it to sha256, then call hexdigest() on the resulting hash object to get the hex string. The closing parenthesis of sha256( comes after the encoded bytes, not before .hexdigest().
Building the Blockchain Class and Validating the Chain
The Blockchain class is a thin wrapper around a list of blocks. It creates the genesis block automatically and provides methods for adding new blocks and verifying the chain's integrity.
class Blockchain:
DIFFICULTY = 3
def __init__(self):
self.chain = []
self._create_genesis_block()
def _create_genesis_block(self):
genesis = Block(0, "Genesis Block", "0" * 64)
genesis.mine_block(self.DIFFICULTY)
self.chain.append(genesis)
def add_block(self, data):
last_block = self.chain[-1]
new_block = Block(len(self.chain), data, last_block.hash)
new_block.mine_block(self.DIFFICULTY)
self.chain.append(new_block)
def is_valid(self):
for i in range(1, len(self.chain)):
current = self.chain[i]
previous = self.chain[i - 1]
# Recompute the current block's hash
if current.hash != current.compute_hash():
print(f"Block {i} hash mismatch — data has been altered.")
return False
# Confirm the linkage
if current.previous_hash != previous.hash:
print(f"Block {i} previous_hash mismatch — chain is broken.")
return False
return True
The genesis block uses "0" * 64 as its previous_hash — a string of 64 zeros that signals there is no real predecessor. Every other block reads self.chain[-1].hash to get the most recent block's hash before creating the new one.
"SHA-256 produces a 256-bit hash value." — Python Software Foundation, hashlib documentation
Running the complete example
Putting the two classes together, here is how you instantiate the blockchain, add blocks, verify the chain, and then demonstrate what happens when a block is tampered with.
# --- Run the blockchain ---
bc = Blockchain()
bc.add_block({"sender": "Alice", "receiver": "Bob", "amount": 50})
bc.add_block({"sender": "Bob", "receiver": "Carol", "amount": 25})
bc.add_block({"sender": "Carol", "receiver": "Alice", "amount": 10})
print("\n--- Chain valid?", bc.is_valid())
# Tamper with block 1 and check again
bc.chain[1].data = {"sender": "Alice", "receiver": "Bob", "amount": 9999}
print("--- After tampering, chain valid?", bc.is_valid())
When you run this, you should see three mining messages, a True validity check, and then a mismatch error followed by False once the data in block 1 is changed. The validator caught the attack because it recomputed block 1's hash and found it no longer matched the stored value.
After modifying block.data in Python you can also call block.hash = block.compute_hash() to update the stored hash. The chain will still break because block 2's previous_hash still holds the old value. This is an important thing to test — it shows why linkage checking is a separate step from hash recomputation.
The mine_block method below is supposed to keep incrementing the nonce until the hash starts with the required zeros. One line has a bug that makes the loop run forever. Click it.
self.nonce = 0 to self.nonce += 1. Resetting the nonce to zero inside the loop means the hash computation starts from the same input every iteration. The hash never changes, the condition never becomes true, and the loop never exits. Incrementing the nonce changes the input to compute_hash on each pass, producing a new hash each time until the target is met.
prev field stores the hash of the block to its left, creating the tamper-evident linkage.How to Build a Simple Blockchain in Python
Follow these five steps to go from an empty file to a working, tamper-detectable blockchain. Each step builds directly on the last.
-
Create the Block class
Define a
Blockclass whose__init__method acceptsindex,data, andprevious_hash. Setself.timestampusingdatetime.datetime.utcnow(), initializeself.nonce = 0, and assignself.hash = self.compute_hash(). Thecompute_hashmethod concatenates all fields into a string, encodes it, and returns the SHA-256 hex digest. -
Add proof of work to the Block
Write a
mine_block(self, difficulty)method. Build atargetstring ofdifficultyzeros. Loop withwhile not self.hash.startswith(target), incrementingself.nonceand recomputingself.hasheach iteration. Print the nonce and hash when the loop exits. -
Create the Blockchain class
Define a
Blockchainclass with a class-levelDIFFICULTY = 3constant and an__init__that creates an empty list and calls_create_genesis_block. That private method instantiatesBlock(0, "Genesis Block", "0" * 64), mines it, and appends it toself.chain. Theadd_block(self, data)method reads the last block's hash, creates a new block, mines it, and appends. -
Implement chain validation
Write
is_valid(self). Loop from index 1 to the end of the chain. For each block, callcurrent.compute_hash()and compare the result tocurrent.hash. Also comparecurrent.previous_hashtoprevious.hash. ReturnFalsewith a descriptive message on any mismatch, and returnTrueonly if all checks pass. -
Run and test the blockchain
Instantiate
Blockchain(), calladd_blockseveral times with dictionary data, and callis_valid()to confirm the chain is intact. Then modifybc.chain[1].datadirectly and callis_valid()again. The validator should report a hash mismatch on block 1 and returnFalse, confirming that tamper detection works.
Python Learning Summary Points
- A blockchain is a list of blocks where each block stores the hash of its predecessor. This linkage makes retroactive alteration detectable because changing any block's contents changes its hash, which no longer matches the
previous_hashstored in the following block. - Python's built-in
hashlibmodule provides SHA-256 hashing with no external dependencies. Pass a byte-encoded string tohashlib.sha256(...).hexdigest()to get a deterministic 64-character hex output. - Proof of work adds a computational cost to block creation by requiring the hash to satisfy a difficulty target. The nonce is the only variable in the hash input that the miner controls, so the miner increments it until the hash meets the target.
- Chain validation requires two separate checks per block: that the block's stored hash still matches a freshly computed hash of its current contents, and that its
previous_hashfield still matches the actual hash of the preceding block. - This tutorial is a conceptual model, not a production system. Real blockchains add peer-to-peer networking, consensus algorithms, digital signatures for transaction authorization, and many additional layers of security and reliability.
From here you can extend this foundation in several directions. Replace the data dictionary with a simple Transaction class. Add a list of pending transactions to the Blockchain that get bundled into the next mined block. Serialize the chain to JSON and reload it. Each of these extensions will reinforce the same concepts you used here — classes, hashing, iteration, and validation — while showing you how far a few Python primitives can take you.
Frequently Asked Questions
A blockchain is a list of records called blocks, where each block stores data and a cryptographic hash of the previous block. This chain of hashes makes it extremely difficult to alter any past record without invalidating every block that follows it.
For a basic blockchain you only need two modules from Python's standard library: hashlib to compute SHA-256 hashes and datetime to timestamp each block. No third-party packages are required.
The genesis block is the first block in a blockchain. It has no previous block to reference, so its previous_hash field is typically set to a string of zeros or an empty string. Every subsequent block traces its lineage back to this starting point.
Proof of work requires a block's hash to start with a specific number of leading zeros. To meet this requirement, the miner must repeatedly increment a number called a nonce until the resulting hash satisfies the target. This computation takes real time and resources, making it costly to add blocks at will.
No. This tutorial builds a minimal blockchain to illustrate the core concepts: blocks, hashing, chaining, and proof of work. Real networks like Bitcoin add peer-to-peer networking, consensus rules, digital signatures, transaction validation, and many other layers on top of these fundamentals.
Loop through each block and check two things: that the block's stored hash matches a freshly computed hash of its contents, and that the previous_hash field matches the actual hash of the preceding block. If either check fails at any position, the chain has been altered.
A nonce is a number that miners increment during proof of work until the block's hash meets the required target. The word comes from "number used once." In this tutorial, nonce starts at zero and counts up until the SHA-256 hash of the block begins with the required number of leading zeros.
SHA-256 produces a fixed 64-character hexadecimal output regardless of input size, is deterministic, and is computationally infeasible to reverse. Changing even one character in the input produces a completely different hash, which is what makes the chain tamper-evident.