Short answer (TL;DR)
This is because in the first test the CPython implementation of dict will create a new dict from the list, but the second only copies the dictionary. Copying takes less time than parsing the list.
Additional info
Consider this code:
import dis
dis.dis("dict([('foo', 1), ('bar', 'bar'), ('baz', 100)])", depth=10)
print("------------")
dis.dis("dict({'foo': 1, 'bar': 'bar', 'baz': 100})", depth=10)
Where
The dis module supports the analysis of CPython bytecode by
disassembling it.
Which lets us see the bytecode operations performed. Output shows
1 0 LOAD_NAME 0 (dict)
2 LOAD_CONST 0 (('foo', 1))
4 LOAD_CONST 1 (('bar', 'bar'))
6 LOAD_CONST 2 (('baz', 100))
8 BUILD_LIST 3
10 CALL_FUNCTION 1
12 RETURN_VALUE
------------
1 0 LOAD_NAME 0 (dict)
2 LOAD_CONST 0 (1)
4 LOAD_CONST 1 ('bar')
6 LOAD_CONST 2 (100)
8 LOAD_CONST 3 (('foo', 'bar', 'baz'))
10 BUILD_CONST_KEY_MAP 3
12 CALL_FUNCTION 1
14 RETURN_VALUE
From the output you can see:
- Both calls need to load the
dict name that is going to be called.
- After that, the first method loads a list to memory (
BUILD_LIST) whereas the second builds a dictionary (BUILD_CONST_KEY_MAP) (see here)
- For that reason, when the dict function is called (the
CALL_FUNCTION step (see here)), it takes much shorter in the second case, because the dictionary has been already created so it simply makes a copy instead of having to iterate over the list to create a hash table.
Note: with the bytecode you can't conclusively decide that CALL_FUNCTION does that, since its implementation is written in C and only by reading it you can actually know that (see Martijn Pieters' answer for an accurate explanation on how this part works). However, it helps see how the dictionary object is already created outside dict() (step-wise, not syntactically-wise in the example), while as for the list, this is not the case.
Edit
To be clear, when you say
There are a couple of ways to construct a dictionary in python
It is true that by doing:
dkwargs = {'foo': 1, 'bar': 'bar', 'baz': 100}
You are creating a dictionary, in the sense that the interpreter transforms an expression into a dictionary object stored in memory, and makes the variable dkwargs point to it. However, by doing: dict(**kwargs) or if you prefer dict(kwargs), you are not really creating a dictionary, but just copying an already existing object (and it's important to emphasise copying):
>>> dict(dkwargs) is dkwargs
False
dict(kwargs) forces Python to create a new object; however, that does not mean it has to re-build the object. In fact, that operation is useless because in practice they are equal objects (though not the same object).
>>> id(dkwargs)
2787648914560
>>> new_dict = dict(dkwargs)
>>> id(new_dict)
2787652299584
>>> new_dict == dkwargs
True
>>> id(dkwargs) is id(new_dict)
False
Where id:
Return the “identity” of an object. This is an integer which is guaranteed to be unique and constant for this object during its lifetime [...]
CPython implementation detail: This is the address of the object in memory.
Unless, of course, you want to specifically duplicate the object in order to modify one so edits are not linked to the other reference.