Repository: ethereum/web3.py

Issue:
ContractFunction.call may fail as args and kwargs properties may be None
* Version: 5.29.2
* Python: 3.10.4
* OS: linux


### What was wrong?

Depending on the way how a `ContractFunction` is instantiated, its `args` and `kwargs` may be `None`, which occurs errors when invoking `call`.

#### Example code

```python
import json
from web3.auto.infura import w3

USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
EIP20_ABI = json.loads('[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Approval","type":"event"}]')

contract = w3.eth.contract(USDC, abi=EIP20_ABI)
f = contract.get_function_by_signature("decimals()")
f.call()
```

#### Result
```
Traceback (most recent call last):
  File "...", line 14, in <module>
    f.call()
  File ".../venv/lib/python3.10/site-packages/web3/contract.py", line 957, in call
    return call_contract_function(
TypeError: Value after * must be an iterable, not NoneType

```


### How can it be fixed?

The minimal fix may be changing `*self.args` and  `**self.kwargs` to `*(self.args or [])` and `**(self.kwargs or {})` in the following lines: 

https://github.com/ethereum/web3.py/blob/c68f6564ef85c17c5162ce2493d6db16e755516c/web3/contract.py#L1222-L1223

I can work on creating a PR.

Discussion:

Patch:
diff --git a/docs/web3.contract.rst b/docs/web3.contract.rst
index c8095baf..bdbad716 100644
--- a/docs/web3.contract.rst
+++ b/docs/web3.contract.rst
@@ -679,6 +679,16 @@ If you have the function name in a variable, you might prefer this alternative:
         contract_func = myContract.functions[func_to_call]
         twentyone = contract_func(3).call()
 
+You can also interact with contract functions without parentheses if the function doesn't
+take any arguments. For example:
+
+    .. code-block:: python
+
+        >>> myContract.functions.return13.call()
+        13
+        >>> myContract.functions.return13().call()
+        13
+
 :py:class:`ContractFunction` provides methods to interact with contract functions.
 Positional and keyword arguments supplied to the contract function subclass
 will be used to find the contract function by signature,
diff --git a/newsfragments/3444.feature.rst b/newsfragments/3444.feature.rst
new file mode 100644
index 00000000..1c4caeb8
--- /dev/null
+++ b/newsfragments/3444.feature.rst
@@ -0,0 +1,1 @@
+Allow user to call ContractFunctions without parentheses
diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py
index 2c8593da..cbd56abe 100644
--- a/web3/contract/async_contract.py
+++ b/web3/contract/async_contract.py
@@ -319,8 +319,8 @@ class AsyncContractFunction(BaseContractFunction):
             state_override,
             ccip_read_enabled,
             self.decode_tuples,
-            *self.args,
-            **self.kwargs,
+            *self.args or (),
+            **self.kwargs or {},
         )
 
     async def transact(self, transaction: Optional[TxParams] = None) -> HexBytes:
@@ -332,8 +332,8 @@ class AsyncContractFunction(BaseContractFunction):
             setup_transaction,
             self.contract_abi,
             self.abi,
-            *self.args,
-            **self.kwargs,
+            *self.args or (),
+            **self.kwargs or {},
         )
 
     async def estimate_gas(
@@ -352,8 +352,8 @@ class AsyncContractFunction(BaseContractFunction):
             self.abi,
             block_identifier,
             state_override,
-            *self.args,
-            **self.kwargs,
+            *self.args or (),
+            **self.kwargs or {},
         )
 
     async def build_transaction(
@@ -367,8 +367,8 @@ class AsyncContractFunction(BaseContractFunction):
             built_transaction,
             self.contract_abi,
             self.abi,
-            *self.args,
-            **self.kwargs,
+            *self.args or (),
+            **self.kwargs or {},
         )
 
     @staticmethod
diff --git a/web3/contract/contract.py b/web3/contract/contract.py
index dcdee08c..e81fcbf3 100644
--- a/web3/contract/contract.py
+++ b/web3/contract/contract.py
@@ -318,12 +318,13 @@ class ContractFunction(BaseContractFunction):
             state_override,
             ccip_read_enabled,
             self.decode_tuples,
-            *self.args,
-            **self.kwargs,
+            *self.args or (),
+            **self.kwargs or {},
         )
 
     def transact(self, transaction: Optional[TxParams] = None) -> HexBytes:
         setup_transaction = self._transact(transaction)
+
         return transact_with_contract_function(
             self.address,
             self.w3,
@@ -331,8 +332,8 @@ class ContractFunction(BaseContractFunction):
             setup_transaction,
             self.contract_abi,
             self.abi,
-            *self.args,
-            **self.kwargs,
+            *self.args or (),
+            **self.kwargs or {},
         )
 
     def estimate_gas(
@@ -351,12 +352,13 @@ class ContractFunction(BaseContractFunction):
             self.abi,
             block_identifier,
             state_override,
-            *self.args,
-            **self.kwargs,
+            *self.args or (),
+            **self.kwargs or {},
         )
 
     def build_transaction(self, transaction: Optional[TxParams] = None) -> TxParams:
         built_transaction = self._build_transaction(transaction)
+
         return build_transaction_for_function(
             self.address,
             self.w3,
@@ -364,8 +366,8 @@ class ContractFunction(BaseContractFunction):
             built_transaction,
             self.contract_abi,
             self.abi,
-            *self.args,
-            **self.kwargs,
+            *self.args or (),
+            **self.kwargs or {},
         )
 
     @staticmethod


Test patch:
diff --git a/tests/core/contracts/test_contract_build_transaction.py b/tests/core/contracts/test_contract_build_transaction.py
index dc6226ba..a49d60ef 100644
--- a/tests/core/contracts/test_contract_build_transaction.py
+++ b/tests/core/contracts/test_contract_build_transaction.py
@@ -53,6 +53,20 @@ def test_build_transaction_with_contract_no_arguments(
     }
 
 
+def test_build_transaction_with_contract_no_arguments_no_parens(
+    w3, math_contract, build_transaction
+):
+    txn = math_contract.functions.incrementCounter.build_transaction()
+    assert dissoc(txn, "gas") == {
+        "to": math_contract.address,
+        "data": "0x5b34b966",
+        "value": 0,
+        "maxFeePerGas": 2750000000,
+        "maxPriorityFeePerGas": 10**9,
+        "chainId": 131277322940537,
+    }
+
+
 def test_build_transaction_with_contract_fallback_function(
     w3, fallback_function_contract
 ):
@@ -309,6 +323,21 @@ async def test_async_build_transaction_with_contract_no_arguments(
     }
 
 
+@pytest.mark.asyncio
+async def test_async_build_transaction_with_contract_no_arguments_no_parens(
+    async_w3, async_math_contract, async_build_transaction
+):
+    txn = await async_math_contract.functions.incrementCounter.build_transaction()
+    assert dissoc(txn, "gas") == {
+        "to": async_math_contract.address,
+        "data": "0x5b34b966",
+        "value": 0,
+        "maxFeePerGas": 2750000000,
+        "maxPriorityFeePerGas": 10**9,
+        "chainId": 131277322940537,
+    }
+
+
 @pytest.mark.asyncio
 async def test_async_build_transaction_with_contract_fallback_function(
     async_w3, async_fallback_function_contract
diff --git a/tests/core/contracts/test_contract_call_interface.py b/tests/core/contracts/test_contract_call_interface.py
index 6f187d7d..7b3e9350 100644
--- a/tests/core/contracts/test_contract_call_interface.py
+++ b/tests/core/contracts/test_contract_call_interface.py
@@ -145,6 +145,10 @@ def test_call_with_no_arguments(math_contract, call):
     assert result == 13
 
 
+def test_call_no_arguments_no_parens(math_contract):
+    assert math_contract.functions.return13.call() == 13
+
+
 def test_call_with_one_argument(math_contract, call):
     result = call(contract=math_contract, contract_function="multiply7", func_args=[3])
     assert result == 21
@@ -2287,6 +2291,12 @@ async def test_async_call_with_no_arguments(async_math_contract, call):
     assert result == 13
 
 
+@pytest.mark.asyncio
+async def test_async_call_with_no_arguments_no_parens(async_math_contract, call):
+    result = await async_math_contract.functions.return13.call()
+    assert result == 13
+
+
 @pytest.mark.asyncio
 async def test_async_call_with_one_argument(async_math_contract, call):
     result = await async_math_contract.functions.multiply7(3).call()
diff --git a/tests/core/contracts/test_contract_estimate_gas.py b/tests/core/contracts/test_contract_estimate_gas.py
index 730b2353..993b9ded 100644
--- a/tests/core/contracts/test_contract_estimate_gas.py
+++ b/tests/core/contracts/test_contract_estimate_gas.py
@@ -18,6 +18,19 @@ def test_contract_estimate_gas(w3, math_contract, estimate_gas, transact):
     assert abs(gas_estimate - gas_used) < 21000
 
 
+def test_estimate_gas_can_be_called_without_parens(
+    w3, math_contract, estimate_gas, transact
+):
+    gas_estimate = math_contract.functions.incrementCounter.estimate_gas()
+
+    txn_hash = transact(contract=math_contract, contract_function="incrementCounter")
+
+    txn_receipt = w3.eth.wait_for_transaction_receipt(txn_hash)
+    gas_used = txn_receipt.get("gasUsed")
+
+    assert abs(gas_estimate - gas_used) < 21000
+
+
 def test_contract_fallback_estimate_gas(w3, fallback_function_contract):
     gas_estimate = fallback_function_contract.fallback.estimate_gas()
 
@@ -197,6 +210,24 @@ async def test_async_estimate_gas_accepts_latest_block(
     assert abs(gas_estimate - gas_used) < 21000
 
 
+@pytest.mark.asyncio
+async def test_async_estimate_gas_can_be_called_without_parens(
+    async_w3, async_math_contract, async_transact
+):
+    gas_estimate = await async_math_contract.functions.counter.estimate_gas(
+        block_identifier="latest"
+    )
+
+    txn_hash = await async_transact(
+        contract=async_math_contract, contract_function="incrementCounter"
+    )
+
+    txn_receipt = await async_w3.eth.wait_for_transaction_receipt(txn_hash)
+    gas_used = txn_receipt.get("gasUsed")
+
+    assert abs(gas_estimate - gas_used) < 21000
+
+
 @pytest.mark.asyncio
 async def test_async_estimate_gas_block_identifier_unique_estimates(
     async_w3, async_math_contract, async_transact
diff --git a/tests/core/contracts/test_contract_transact_interface.py b/tests/core/contracts/test_contract_transact_interface.py
index 215443bc..ab46a49a 100644
--- a/tests/core/contracts/test_contract_transact_interface.py
+++ b/tests/core/contracts/test_contract_transact_interface.py
@@ -285,6 +285,17 @@ def test_receive_contract_with_fallback_function(receive_function_contract, call
     assert final_value == "receive"
 
 
+def test_contract_can_transact_without_fn_parens(w3, math_contract, call):
+    initial_value = call(contract=math_contract, contract_function="counter")
+    txn_hash = math_contract.functions.incrementCounter.transact()
+    txn_receipt = w3.eth.wait_for_transaction_receipt(txn_hash)
+    assert txn_receipt is not None
+
+    final_value = call(contract=math_contract, contract_function="counter")
+
+    assert final_value - initial_value == 1
+
+
 @pytest.mark.asyncio
 async def test_async_transacting_with_contract_no_arguments(
     async_w3, async_math_contract, async_transact, async_call
@@ -306,6 +317,25 @@ async def test_async_transacting_with_contract_no_arguments(
     assert final_value - initial_value == 1
 
 
+@pytest.mark.asyncio
+async def test_async_transacting_with_contract_no_arguments_no_parens(
+    async_w3, async_math_contract, async_transact, async_call
+):
+    initial_value = await async_call(
+        contract=async_math_contract, contract_function="counter"
+    )
+
+    txn_hash = await async_math_contract.functions.incrementCounter.transact()
+    txn_receipt = await async_w3.eth.wait_for_transaction_receipt(txn_hash)
+    assert txn_receipt is not None
+
+    final_value = await async_call(
+        contract=async_math_contract, contract_function="counter"
+    )
+
+    assert final_value - initial_value == 1
+
+
 @pytest.mark.asyncio
 async def test_async_transact_not_sending_ether_to_nonpayable_function(
     async_w3, async_payable_tester_contract, async_transact, async_call