Any configuration tool is going to have a "configuration language". Puppet defined it's own language. It was neat (even revolutionary). Instead of writing scripts to start/stop services and add/remove users, you just declared what Resources (users, services, files) you wanted, and Puppet "made it so". Your declarations are fairly simple, and the Puppet engine implements them in an idempotent way (so it only does what needs doing. By default it runs every 30 minutes to implement any new changes).
But the Puppet "config language" had to be extended in a number of different directions. People need to be able to express complex things like "All servers must have this program installed, except the staging/test servers". Worse, some configuration is repetitive (think vhosts for a webhost). Puppet has a limited way of dealing with that. You can quickly devolve into generating your Puppet config, which is the wrong answer.
Chef started copying the resource declaration style of Puppet. But they implemented the config files as Ruby with extensions. Unlike Python or Perl, you can write very simple to understand config files that are actually valid Ruby. But writing in Ruby allows you to DRY (Don't Repeat Yourself) your configuration. Not just simple variable substitutions, but loops anywhere you want them, calling out to external services (databases, parsing custom config files), etc. Like Rails, Chef gives you a pre-made directory structure (definitions, attributes, recipes, etc.). It looks complex, but it's nice because "everything has a place" and it keeps you organized.
Chef is very useful when building complex apps "in the cloud" (i.e. on Amazon EC2 where servers come and go).