The fastest way to understand a blockchain is to build a small one. In this tutorial, you will create an educational blockchain in Python, connect it to a Flask API, add transactions, implement a simplified proof-of-work puzzle, run two local nodes, and resolve conflicting chains.
This project demonstrates how blocks, hashes, pending transactions, mining, and basic chain reconciliation fit together. It does not create a secure cryptocurrency or a production-ready decentralized network. That distinction matters especially if you are evaluating blockchain projects as investments: a linked list of hashed records is not the same thing as a secure financial system.
As an Amazon Associate I earn from qualifying purchases.
The exercise comes from Daniel van Flymen’s tutorial “Learn Blockchains by Building One,” published September 24, 2017. The original walkthrough remains useful, but its Python and dependency instructions are historical and should not be treated as current setup guidance.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →What you will build
By the end, your local project will contain a toy blockchain node with:
#1 Best Overall
- A chain of linked blocks.
- A genesis block that starts the chain.
- Pending transactions waiting for inclusion in a block.
- SHA-256 hashing and previous-block references.
- A simple proof-of-work mining loop.
- HTTP endpoints for submitting transactions and inspecting the chain.
- Node registration and simplified longest-valid-chain reconciliation.
The result is valuable as a learning model. It is not a deployable cryptocurrency because it lacks digital signatures, secure identity, persistent storage, authenticated peer networking, economic incentives, robust fork choice, and several other properties required by real systems.
Prerequisites
You do not need prior blockchain expertise. You should be comfortable with:
- Basic Python syntax, functions, classes, lists, dictionaries, loops, and sets.
- JSON and basic command-line use.
- HTTP methods, particularly
GETandPOST. - The general idea that a hash is a fixed-length fingerprint of data.
Virtual environments, REST APIs, SHA-256, localhost networking, and basic testing are helpful but optional.
Use a current Python environment
The original tutorial specifies Python 3.6, Flask 0.12.2, and Requests 2.18.4. Those are 2017-era versions. The original repository documents a Pipenv-based setup, while the associated book repository uses Poetry and reflects a Python 3.8-era environment. For a new project, use an isolated environment and a supported Python release. The Python downloads page identifies Python 3.14 as the current bugfix line in the referenced research snapshot, with Python 3.12 and 3.13 also supported; Python 3.9 is end-of-life.
macOS or Linux
mkdir learn-blockchain
cd learn-blockchain
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install Flask
Windows PowerShell
mkdir learn-blockchain
cd learn-blockchain
py -m venv .venv
.venvScriptsActivate.ps1
python -m pip install --upgrade pip
pip install Flask
The current Flask documentation is the appropriate reference for maintained Flask behavior.
Reproducing the historical tutorial
If your goal is to reproduce the original 2017 environment rather than build a maintained version, the article specifies:
pip install Flask==0.12.2 requests==2.18.4
Use this only as a legacy reproduction path. Old packages may fail to install or behave correctly on modern Python releases. Do not mix old pins into a new production project.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
How the model works
Blocks and hash links
Each block in the tutorial contains:
index: the block’s position.timestamp: when it was created.transactions: transfers included in that block.proof: the result of the proof-of-work puzzle.previous_hash: the hash of the preceding block.
When a block stores the previous block’s hash, changing an earlier block changes its hash and makes later references inconsistent. This makes tampering detectable within the toy model.
It does not make records absolutely immutable. Real systems can experience reorganizations, competing histories, database compromise, key compromise, censorship, or control by a majority of the relevant consensus power. Hashing provides integrity evidence; it does not provide authorization, confidentiality, or financial safety.
Rank #2
The genesis block
The genesis block is the deliberate starting point of the chain. It has no genuine predecessor, so the implementation can create it with a fixed proof and a simple previous-hash value.
Every node in a real protocol must agree on the genesis block’s exact contents. Its fields, serialization, and hash must be deterministic. Otherwise, two nodes may begin with different histories. A toy implementation can be simpler, but the reason for the starting block remains the same: every later block needs a common reference point.
Build the blockchain core
Create a file named blockchain.py. The following compact implementation follows the tutorial’s learning sequence while adding basic validation and modern Python structure.
import hashlib
import json
import time
from urllib.parse import urlparse
class Blockchain:
def __init__(self):
self.current_transactions = []
self.chain = []
self.nodes = set()
# Create the genesis block.
self.new_block(proof=100, previous_hash="1")
def register_node(self, address):
parsed = urlparse(address)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise ValueError("A node must be a complete HTTP or HTTPS URL")
self.nodes.add(parsed.netloc)
def new_block(self, proof, previous_hash=None):
block = {
"index": len(self.chain) + 1,
"timestamp": time.time(),
"transactions": self.current_transactions,
"proof": proof,
"previous_hash": previous_hash or self.hash(self.chain[-1]),
}
self.current_transactions = []
self.chain.append(block)
return block
def new_transaction(self, sender, recipient, amount):
if not sender or not recipient:
raise ValueError("sender and recipient are required")
if isinstance(amount, bool) or not isinstance(amount, (int, float)):
raise ValueError("amount must be numeric")
if amount < 0:
raise ValueError("amount cannot be negative")
self.current_transactions.append({
"sender": sender,
"recipient": recipient,
"amount": amount,
})
return self.last_block["index"] + 1
@property
def last_block(self):
return self.chain[-1]
@staticmethod
def hash(block):
# sort_keys gives equivalent dictionaries the same JSON field order.
block_string = json.dumps(block, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(block_string.encode()).hexdigest()
@staticmethod
def valid_proof(last_proof, proof):
guess = f"{last_proof}{proof}".encode()
guess_hash = hashlib.sha256(guess).hexdigest()
return guess_hash[:4] == "0000"
def proof_of_work(self, last_proof):
proof = 0
while not self.valid_proof(last_proof, proof):
proof += 1
return proof
@classmethod
def valid_chain(cls, chain):
if not chain:
return False
for index in range(1, len(chain)):
previous = chain[index - 1]
current = chain[index]
if current["previous_hash"] != cls.hash(previous):
return False
if not cls.valid_proof(previous["proof"], current["proof"]):
return False
return True
Why canonical serialization matters
A hash is calculated from bytes, not from an abstract Python dictionary. Two nodes must serialize semantically identical blocks into exactly the same bytes. The example uses JSON with sorted keys and compact separators so field order is predictable.
A production protocol must define much more: number formatting, character encoding, null handling, signature encoding, field inclusion, and treatment of mutable values. Without those rules, two implementations can appear to hold the same block but calculate different hashes.
Adding transactions
A transaction has the illustrative shape:
{
"sender": "my address",
"recipient": "someone else's address",
"amount": 5
}
The transaction goes into current_transactions and is assigned to the next block that gets mined.
Recommended Free Tools
Notice what this code does not prove. The sender field is merely text. Anyone can claim to be another user, there is no signature, no balance check, and no account or unspent-output model. Basic input validation prevents obvious malformed data, but it does not create a secure currency. Real authorization requires public/private-key cryptography and signature verification.
Proof of work
The simplified puzzle searches for a number whose combination with the previous proof produces a SHA-256 hash beginning with four zeroes:
def valid_proof(last_proof, proof):
guess = f"{last_proof}{proof}".encode()
guess_hash = hashlib.sha256(guess).hexdigest()
return guess_hash[:4] == "0000"
Finding a proof requires repeated guesses, while checking one is comparatively cheap. Requiring more leading zeroes makes a valid result rarer and generally increases the expected work.
This is a teaching device, not a measurement of cryptocurrency security. The toy chain has no meaningful economic reward, no network-wide difficulty adjustment, no competitive mining market, and no hash-rate security model. Do not compare its four-zero rule directly with Bitcoin mining difficulty.
Free tools Windows power users keep installed
One-click scans. No signup required.
Expose the chain through Flask
Add the API layer below to the same file:
from flask import Flask, jsonify, request
app = Flask(__name__)
blockchain = Blockchain()
@app.get("/chain")
def full_chain():
return jsonify({
"chain": blockchain.chain,
"length": len(blockchain.chain),
})
@app.post("/transactions/new")
def new_transaction():
values = request.get_json(silent=True) or {}
required = {"sender", "recipient", "amount"}
if not required.issubset(values):
return jsonify({"error": "Missing values"}), 400
try:
index = blockchain.new_transaction(
values["sender"], values["recipient"], values["amount"]
)
except ValueError as error:
return jsonify({"error": str(error)}), 400
return jsonify({"message": f"Transaction will be added to Block {index}"}), 201
@app.get("/mine")
def mine():
last_proof = blockchain.last_block["proof"]
proof = blockchain.proof_of_work(last_proof)
# This is a toy reward, not a real currency issuance mechanism.
blockchain.new_transaction(
sender="0",
recipient="toy-miner",
amount=1,
)
block = blockchain.new_block(proof)
return jsonify({
"message": "New block forged",
"index": block["index"],
"transactions": block["transactions"],
"proof": block["proof"],
"previous_hash": block["previous_hash"],
}), 200
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-p", "--port", default=5000, type=int)
args = parser.parse_args()
app.run(host="127.0.0.1", port=args.port)
The original tutorial uses GET /mine for convenience. In a modern API, mining changes server state, so POST /mine would usually better match HTTP semantics. You can preserve the historical route for compatibility while designing new code with a state-changing method.
Run and test the node
Start the server:
python blockchain.py
The original example uses port 5000 and displays a local address similar to:
Running on http://127.0.0.1:5000/
Mine a block
curl http://127.0.0.1:5000/mine
The response should contain a new block index, its proof, its previous hash, and the transactions included in it. The proof-of-work loop may take noticeably longer as you increase the difficulty.
Submit a transaction
curl -X POST
-H "Content-Type: application/json"
-d '{
"sender": "d4ee26eee15148ee92c6cd394edd974e",
"recipient": "someone-other-address",
"amount": 5
}'
http://127.0.0.1:5000/transactions/new
A successful response identifies the block number that will receive the transaction. The transaction remains pending until another block is mined.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Inspect the chain
curl http://127.0.0.1:5000/chain
Use the returned chain array to verify that the block’s previous_hash matches the hash of the preceding block and that transactions moved out of the pending list when mining occurred.
Postman can provide a graphical interface, but it is optional. cURL is sufficient, reproducible, and requires no account. The same requests can also be sent with HTTPie or an editor’s REST-client extension.
Run two nodes
Open two terminals, activate the virtual environment in both, and start separate processes:
python blockchain.py -p 5000
python blockchain.py -p 5001
Each process has its own in-memory chain. They are separate nodes only in the limited sense that they are separate local programs communicating over HTTP. Multiple localhost processes are not equivalent to a permissionless decentralized network.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Register a neighboring node
curl -X POST
-H "Content-Type: application/json"
-d '{"nodes": ["http://127.0.0.1:5001"]}'
http://127.0.0.1:5000/nodes/register
The original tutorial maintains a registry of neighboring node URLs. A production implementation would need URL validation, authentication or admission controls, TLS, request timeouts, retry limits, peer discovery, and protection against SSRF and malicious peers.
Resolve a conflict
The tutorial’s simplified rule is: ask neighboring nodes for their chains, validate them, and replace the local chain if a neighbor has a longer valid chain.
Conceptually, the resolver looks like this:
def resolve_conflicts(blockchain):
neighbors = blockchain.nodes
longest = blockchain.chain
for node in neighbors:
# A real implementation should use timeouts and handle failures.
response = requests.get(f"http://{node}/chain", timeout=3)
if response.status_code != 200:
continue
data = response.json()
chain = data.get("chain", [])
if len(chain) > len(longest) and blockchain.valid_chain(chain):
longest = chain
if longest is not blockchain.chain:
blockchain.chain = longest
return True
return False
This rule is useful for demonstrating reconciliation, but “the longest chain wins” is not a universal blockchain consensus rule. Real systems may use accumulated proof-of-work, stake-weighted fork choice, finality mechanisms, validator rules, or other protocol-specific criteria. Chain length alone can be misleading.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Break the toy chain deliberately
Learning improves when you test failure cases rather than only the happy path.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsTamper with an earlier block
Change a transaction or timestamp in an existing block, then call valid_chain. The hash of that block no longer matches the next block’s previous_hash, so validation should return False.
This demonstrates tamper evidence. It does not prevent an attacker from rewriting the entire history and presenting a replacement chain if the surrounding protocol permits it.
Submit malformed JSON
curl -X POST
-H "Content-Type: application/json"
-d '{"sender":"only-one-field"}'
http://127.0.0.1:5000/transactions/new
The API should return a client error rather than silently creating an incomplete transaction. A production API should also enforce maximum lengths, exact numeric rules, rate limits, authentication, and a formal schema.
Use a negative amount
curl -X POST
-H "Content-Type: application/json"
-d '{"sender":"a","recipient":"b","amount":-5}'
http://127.0.0.1:5000/transactions/new
The example rejects negative values, but this is still not balance validation. A real ledger must define how balances are created, spent, checked, and protected against double spending.
Use a busy port
If port 5000 is already occupied, the server cannot bind to it. Start another instance on a different port:
Best Value
python blockchain.py -p 5001
The original repository also documents alternate ports such as 5001 and 5002.
Stop a peer
If a registered peer is unavailable, an unprotected HTTP request may hang or fail with a connection error. Add timeouts, catch network exceptions, and treat an unreachable peer as unavailable rather than valid. Never let a single peer prevent local chain inspection or normal API operation.
Restart a node
This in-memory implementation loses its chain when the process stops. Persistence requires a file or database, but persistence alone does not make a system decentralized or secure. You would still need consistency rules, crash recovery, validation, access control, and protection against conflicting writes.
What this project does not implement
| Missing feature | Why it matters |
|---|---|
| Digital signatures | Without signatures, anyone can claim to be any sender. |
| Balance or UTXO validation | The basic model does not prove that a sender owns or can spend funds. |
| Persistent storage | Process restarts erase the in-memory state. |
| Merkle trees | Transactions are stored directly in blocks rather than in a scalable authenticated tree. |
| Peer authentication | Node URLs are not identities, and arbitrary peers can be unsafe. |
| Peer discovery | Nodes must be told about one another manually. |
| Sybil resistance | There is no mechanism to stop an attacker from creating many identities. |
| Difficulty adjustment | The four-zero target does not adapt to changing computing power. |
| Economic incentives | The mining reward has no real monetary value or enforceable monetary policy. |
| Robust fork choice | Longest valid chain is only a teaching simplification. |
| Concurrency control | Simultaneous requests can race to append or mine blocks. |
The project therefore demonstrates several blockchain ingredients without reproducing the complete security model of Bitcoin, Ethereum, or another public network.
Modernize the tutorial responsibly
- Use an isolated environment. Keep dependencies separate from your operating system and record them in a requirements file or modern project configuration.
- Port old code deliberately. The original tutorial repository and the separate book repository are useful references. The book source identifies the finalized implementation under
chapters/chapter_7. - Validate at the API boundary. Reject unknown or malformed fields, enforce numeric ranges, and return consistent error responses.
- Define serialization. Specify canonical encoding before multiple implementations exchange blocks.
- Use appropriate HTTP methods. Preserve
GET /minewhen following the historical tutorial, but preferPOST /minefor new APIs. - Add tests. Test hashing, proof validation, tampering, malformed requests, peer timeouts, and chain replacement.
- Add persistence carefully. A database can preserve state, but it does not solve consensus, identity, or adversarial networking.
- Add operational safeguards. Use structured logs, timeouts, authentication, rate limits, TLS, and concurrency controls in any service exposed beyond localhost.
Choosing the right next step
This tutorial is a good fit if you learn best by implementing data structures and want a compact introduction to blocks, hashing, proof of work, APIs, and basic reconciliation.
It is not enough if your goal is to:
- Build a cryptocurrency.
- Write secure wallets or signed transactions.
- Develop Solidity applications or deploy smart contracts.
- Operate a public peer-to-peer network.
- Understand proof-of-stake systems in depth.
- Design a production ledger or study Byzantine fault tolerance rigorously.
For a more complete cryptocurrency exercise, add signed transactions, wallets, a UTXO or account model, Merkle trees, persistent storage, peer-to-peer message propagation, difficulty adjustment, and explicit fork handling. For Ethereum development, follow a separate path covering Solidity, the EVM, local development networks, testing, deployment, and wallet signing. For consensus theory, study proof of work, proof of stake, Sybil resistance, finality, fork choice, safety, and liveness independently.
Bottom line
“Learn Blockchains by Building One” is still a strong conceptual exercise because it turns abstract terms into inspectable Python code. Build it to understand how hashes link blocks, how pending transactions enter a chain, how proof of work creates a computational puzzle, and how multiple nodes can compare histories.
Free tools Windows power users keep installed
One-click scans. No signup required.
Keep the security boundary clear: this project creates a toy blockchain network, not a secure financial asset. Hash chaining is not transaction authorization, mining is not economic security, and two local Flask processes are not a decentralized public network.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




