Pound sign with DPTR 8051
In assembly language, the term addressing mode doesn't mean taking the address of a variable; but rather how information is encoded into the instruction code. For example, instruction code 0xE8 0x4D is the mov A,0x4D instruction using direct addressing mode: acc A gets the contents of iram address 0x4D. But instruction code 0x74 0x4D is mov A,#0x4D: acc A gets the actual literal value 0x4D. Both instructions are the same length and use similar encoding, but the meaning of the 0x4D depends on which addressing mode was used.

Even if you define a symbol LetterM equ 0x4D, this makes no difference to the 8051 -- mov A,LetterM assembles as 0xE8 0x4D and mov A,#LetterM assembles as 0x74 0x4D. The 8051 itself never sees the assembly source code, only the object code. So the only reason punctuation like # or @ matters at all, is to ensure that the assembler uses the right addressing mode to encode each instruction.

For loading the 16-bit data pointer register, mov dptr,#data16 only supports immediate addressing mode. Usually you want to use dptr to point to the memory address of an object, so immediate addressing mode is what you need. To your question, mov dptr,#Z considers mov dptr,#data16 as the immediate addressing mode, and Z as the address of symbol Z -- which you're using to store a variable.

If you already understand C language pointers, the dptr register is a pointer to an 8-bit value in memory, and the movc A, @A+DPTR instruction is what de-references the pointer value that's in the dptr register. So dptr always gets the address where the variable lives, and the indirect addressing mode (@) uses the value currently in dptr to get the value that dptr is pointing at.

// some C pseudocode to help explain 8051
uint8* dptr_register;      // 8051 built-in DPTR register
uint8 acc_a;               // 8051 built-in accumulator register A
static uint8 myVarZ = 123; // myVarZ: db 123
dptr_register = &amp;myVarZ;   // mov dptr,#myVarZ ; address of myVarZ
acc_a = *dptr_register;    // movc A,@A+DPTR  (assuming A is 0) ; get value of myVarZ


The 8051 is a little bit more complicated though, because it has more than one address space. Code address 0 is different than internal RAM address 0, which is also different than external RAM address 0. This is why there are several different instructions that use dptr (`movc', 'mov', 'movx'). Real C compilers for 8051 (like Keil C51 or SDCC) have a few extra, non-standard keywords to distinguish between these different address spaces.

Other microcontrollers have slightly different instruction sets and use slightly different punctuation, but they're all similar enough that one you learn one, you can easily pick up the others.