To send an EIP-1559 transaction in Python, you build a Type 2 transaction with two fee fields — maxFeePerGas and maxPriorityFeePerGas — sign it with your private key, and broadcast it using web3.py’s send_raw_transaction. That’s the whole flow. What trips people up is how those two fee fields relate to each other, and where the older single-price gasPrice style still fits.
This guide covers both. You’ll get a working web3.py script for a Type 2 transfer, the legacy Type 0 version of the same transfer, and a short comparison of when to reach for each. Everything uses the current web3.py release (v8), whose method and attribute names differ from a lot of the tutorials you’ll find online.
What Is a Type 2 Transaction?
A Type 2 transaction is Ethereum’s dynamic-fee transaction format, introduced by EIP-1559 in the August 2021 London upgrade. It replaced the single gasPrice field with two numbers that split the fee into the part the network sets and the part you choose.
Both of those numbers sit on top of a value called the base fee. Understanding the three pieces is most of the work.
Base fee, priority fee, and max fee — what each one means

- Base fee — a per-gas price the protocol sets for every block and then burns (removes from circulation). It moves up or down by at most 12.5% from one block to the next, depending on how full the previous block was relative to the roughly 15 million gas target. You never set it; the network does. See ethereum.org’s gas documentation for the full mechanism.
- Priority fee (tip) —
maxPriorityFeePerGas, the amount per gas you offer validators on top of the base fee to get your transaction picked up faster. This is the only part that actually goes to the validator. - Max fee —
maxFeePerGas, the absolute ceiling per gas you’re willing to pay, covering both the base fee and your tip. If the base fee lands below that ceiling, you’re charged only what’s needed and the rest is refunded.
The “type” label itself comes from EIP-2718, a typed-envelope scheme: Type 0 is legacy, Type 1 adds an access list (EIP-2930), and Type 2 is the dynamic-fee format this guide focuses on.
Why Did Ethereum Replace the Old Gas Auction?
The legacy model was a blind first-price auction. You set one gasPrice, guessed what the rest of the market was bidding, and hoped it was enough. Bid too low and your transaction stalled for hours; bid too high and you overpaid with no money back.
EIP-1559 took the guessing out of the floor. The base fee gives everyone a published, protocol-set rate that moves in small, predictable steps, and the refund on maxFeePerGas means a generous ceiling no longer means a generous bill. You still compete on the tip when blocks are congested, but you’re no longer bidding in the dark on the base cost.
When Should You Use Legacy Gas Pricing?
On Ethereum mainnet, use Type 2 by default — it’s the modern format, and nearly every wallet, library, and tool supports it. Legacy (Type 0) is worth keeping for two cases: a chain or tool that never adopted EIP-1559, and a fallback when a network temporarily returns incomplete fee data.
The audiences are the same either way. Wallet backends, trading bots, dApp servers, and payment systems all broadcast transactions in code. What changes between the two formats is only the fee fields you fill in.
| Legacy (Type 0) | EIP-1559 (Type 2) | |
|---|---|---|
| Fee fields | gasPrice (one value) | maxFeePerGas + maxPriorityFeePerGas |
| Pricing model | First-price auction — you guess the rate | Protocol base fee + your tip |
| Base fee | None | Set per block, then burned |
| Overpayment | You pay the full gasPrice | Room above base fee + tip is refunded |
| Predictability | Low — volatile, easy to mis-bid | Higher — base fee moves in small steps |
| Typical use today | Non-1559 chains; fallback | Ethereum mainnet and most EVM chains |
How to Send a Type 2 Transaction With web3.py

The flow is five steps: install the library, connect to an Ethereum endpoint, read the current fee data, build and sign the transaction, then broadcast it and wait for the receipt.
Prerequisites and setup
You’ll need Python 3.8 or newer and the web3 package:
bash
pip install web3
You also need an Ethereum endpoint for web3.py to talk to, plus a funded wallet to send from. web3.py doesn’t keep its own copy of the chain — it sends requests to an endpoint that does. NOWNodes provides Ethereum endpoints for exactly this: a free public Ethereum endpoint capped at 5 requests per second for quick tests, or an API key on the free Start plan (100,000 requests) when you want a private endpoint.
One rule before any code: never hardcode or commit a private key. Load it from an environment variable or a secrets manager, and run everything against a testnet first so mistakes cost nothing.
Connect to Ethereum and check the connection
python
import os
from web3 import Web3
# Ethereum endpoint from your NOWNodes dashboard
w3 = Web3(Web3.HTTPProvider(
"https://eth.nownodes.io",
request_kwargs={"headers": {"api-key": "YOUR_API_KEY"}},
))
print(w3.is_connected()) # True when the endpoint responds
is_connected() returns True once your Ethereum endpoint answers. If it comes back False, fix the URL and key before going any further.
Build and broadcast the Type 2 transaction
Here’s the full Type 2 transfer. The comments mark the parts specific to the dynamic-fee format:
python
account = w3.eth.account.from_key(os.environ["PRIVATE_KEY"])
sender = account.address
recipient = Web3.to_checksum_address("0xRecipientAddressHere")
# Current fee data from the network
base_fee = w3.eth.get_block("latest")["baseFeePerGas"]
priority_fee = w3.eth.max_priority_fee # suggested tip, in wei
max_fee = (2 * base_fee) + priority_fee # headroom for a rising base fee
tx = {
"type": 2, # EIP-1559 transaction
"chainId": 1, # 1 = Ethereum mainnet
"from": sender,
"to": recipient,
"value": w3.to_wei(0.01, "ether"),
"nonce": w3.eth.get_transaction_count(sender),
"gas": 21000, # plain ETH transfer
"maxFeePerGas": max_fee,
"maxPriorityFeePerGas": priority_fee,
}
signed = w3.eth.account.sign_transaction(tx, account.key)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
print("Sent:", tx_hash.hex())
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print("Mined in block", receipt["blockNumber"], "- status", receipt["status"])
Two lines do the real work on fees. max_priority_fee asks the network for a reasonable tip. The max_fee formula — twice the current base fee plus the tip — leaves room for the base fee to climb over the next few blocks while your transaction waits. Because the unused portion is refunded, that headroom costs you nothing if the base fee stays flat.
send_raw_transaction relays the signed payload through your endpoint into the network’s mempool. From there, the base fee and validators decide when it’s included — an endpoint broadcasts your transaction, but it doesn’t set the gas or guarantee it lands.
One detail saves a lot of debugging: the signed object exposes its bytes as raw_transaction. Up to web3.py v6 this was rawTransaction, and it changed to raw_transaction in v7. Code copied from older guides breaks here with an AttributeError.
Send the same transfer the legacy way
The Type 0 version swaps the two fee fields for a single gasPrice:
python
tx = {
"type": 0, # legacy transaction
"chainId": 1,
"from": sender,
"to": recipient,
"value": w3.to_wei(0.01, "ether"),
"nonce": w3.eth.get_transaction_count(sender),
"gas": 21000,
"gasPrice": w3.eth.gas_price, # one number, legacy style
}
signed = w3.eth.account.sign_transaction(tx, account.key)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
Signing and broadcasting are identical to the Type 2 version. Only the fee section differs, which is why moving existing code from one format to the other is usually a small change.
Common Mistakes When Setting Gas Fees
- Outdated attribute names. Guides for web3.py 5 or 6 use
signed.rawTransaction. On v7 and v8 that raises anAttributeError— usesigned.raw_transaction. - A hardcoded private key. Keep it in an environment variable, never in a file you commit to a repository.
- A missing
chainId. Without it, signing can fail, and the transaction loses its protection against replay on other networks. Set it to the chain you’re actually on. - Nonce collisions. When you send several transactions quickly, read the nonce once and increment it yourself instead of calling
get_transaction_counteach time — otherwise two transactions can claim the same nonce. - Proof-of-authority chains. Some networks (not Ethereum mainnet) need the
ExtraDataToPOAMiddlewareinjected before requests work. Add it only on the chains that require it.
Key Takeaways
Use Type 2 transactions by default on Ethereum. Pull maxPriorityFeePerGas from the network’s suggestion, give maxFeePerGas enough headroom above the current base fee, and rely on the refund for anything you don’t use. Keep the legacy gasPrice path for chains that don’t support EIP-1559 or as a fallback. And if you’re adapting an older script, check the fee fields and the raw_transaction attribute first — that’s where most broken examples go wrong.
FAQ
Do I need to set the transaction type manually?
No. web3.py infers a Type 2 transaction whenever maxFeePerGas and maxPriorityFeePerGas are present, and a legacy one when only gasPrice is. Setting "type": 2 or "type": 0 is optional, but it makes your intent explicit and avoids surprises if you later mix fields.
Can a transaction still get stuck, and how do I replace it?
Yes. If the base fee rises above your maxFeePerGas, the transaction waits in the mempool until the base fee drops. To push it through sooner, resubmit a transaction with the same nonce and a higher maxFeePerGas and maxPriorityFeePerGas — that replaces the pending one.
Is this fee model available on every EVM chain?
No. Many EVM networks support the two-field format, but some don’t, and a few handle fees differently. If a chain rejects it, fall back to gasPrice. Check the specific network before assuming Type 2 will work.
Does the gas limit differ between the two transaction types?
No. A plain ETH transfer costs 21,000 gas whether you send it as Type 0 or Type 2 — the gas limit reflects the work performed, not the fee format. Only the fee fields change between them.



